package com.sunricher.telinkblemeshlib;

import android.os.Build;
import android.util.Log;

import androidx.annotation.Nullable;

import com.sunricher.telinkblemeshlib.models.DynamicControl;
import com.sunricher.telinkblemeshlib.models.NaturalLight;
import com.sunricher.telinkblemeshlib.util.HexUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

public class MeshCommand {

    /**
     * [0-2]
     */
    private static int seqNo = 1;

    /**
     * [3-4]
     */
    private int src = 0;

    /**
     * [5-6]
     */
    private int dst = 0;

    /**
     * [7], Const.TAG_XX
     */
    private int tag = 0;

    /**
     * [8-9]
     */
    private int vendorId = 0x1102;

    /**
     * [10]
     */
    private int param = 0x10;

    /**
     * [11, 19]
     */
    private byte[] userData;

    private MeshCommand() {

        this.userData = new byte[9];
    }

    static MeshCommand makeWithNotifyData(byte[] data) {

        if (data.length != 20) {
            return null;
        }

        MeshCommand command = new MeshCommand();

        int tempSrc = ((int) data[3] & 0xFF);
        tempSrc |= ((int) data[4] & 0xFF) << 8;
        command.src = tempSrc;

        int tempDst = ((int) data[5]);
        tempDst |= ((int) data[6] & 0xFF) << 8;
        command.dst = tempDst;

        command.tag = ((int) data[7] & 0xFF);

        int tempVendorId = ((int) data[8] & 0xFF) << 8;
        tempVendorId |= ((int) data[9] & 0xFF);
        command.vendorId = tempVendorId;

        command.param = ((int) data[10] & 0xFF);

        for (int i = 0; i < 9; i++) {
            int dataIndex = i + 11;
            command.userData[i] = data[dataIndex];
        }

        return command;
    }

    static MeshCommand makeWithMqttCommandData(byte[] data) {

        if (data.length != 20) {
            return null;
        }

        MeshCommand command = new MeshCommand();

        int tempSrc = ((int) data[3] & 0xFF);
        tempSrc |= ((int) data[4] & 0xFF) << 8;
        command.src = tempSrc;

        int tempDst = ((int) data[5]);
        tempDst |= ((int) data[6] & 0xFF) << 8;
        command.dst = tempDst;

        command.tag = ((int) data[7] & 0xFF);

        int tempVendorId = ((int) data[8] & 0xFF) << 8;
        tempVendorId |= ((int) data[9] & 0xFF);
        command.vendorId = tempVendorId;

        command.param = ((int) data[10] & 0xFF);

        for (int i = 0; i < 9; i++) {
            int dataIndex = i + 11;
            command.userData[i] = data[dataIndex];
        }

        return command;
    }

    /**
     * Telink cmd, request mac address.
     */
    public static MeshCommand requestAddressMac(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_REPLACE_ADDRESS;
        cmd.dst = address;
        cmd.param = 0xFF;
        cmd.userData[0] = (byte) 0xFF;
        cmd.userData[1] = (byte) 0x01;
        cmd.userData[2] = (byte) 0x10;
        return cmd;
    }

    /**
     * Change address
     *
     * @param address    Device address
     * @param newAddress New address
     * @param macData    mac address byte[]
     * @return
     */
    public static MeshCommand changeAddress(int address, int newAddress, byte[] macData) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_REPLACE_ADDRESS;
        cmd.src = 0;
        cmd.dst = address;
        cmd.param = newAddress & 0xFF;
        cmd.userData[0] = 0x00;
        cmd.userData[1] = 0x01;
        cmd.userData[2] = 0x10;
        cmd.userData[3] = macData[5];
        cmd.userData[4] = macData[4];
        cmd.userData[5] = macData[3];
        cmd.userData[6] = macData[2];
        cmd.userData[7] = macData[1];
        cmd.userData[8] = macData[0];
        return cmd;
    }

    /**
     * Only can change the Address.connectNode.
     *
     * @param newAddress
     * @return
     */
    public static MeshCommand changeConnectNodeAddress(int newAddress) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_REPLACE_ADDRESS;
        cmd.src = 0;
        cmd.dst = Address.connectNode;
        cmd.param = newAddress & 0xFF;
        cmd.userData[0] = 0x00;
        return cmd;
    }

    /**
     * Reset to factory network
     *
     * @param address
     * @return
     */
    public static MeshCommand resetNetwork(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_RESET_NETWORK;
        cmd.dst = address;
        // 0x01 reset network name to default value, 0x00 reset to `out_of_mesh`.
        cmd.param = 0x01;
        return cmd;
    }

    /**
     * SR cmd, request mac address and device type.
     */
    public static MeshCommand requestMacDeviceType(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.param = 0x20;
        cmd.userData[0] = (byte) 0x76;
        return cmd;
    }

    /// Change the light type, if the deviceType isnot support change device type, will return a command `requestMacDeviceType`.
    public static MeshCommand changeLightType(int address, MeshDeviceType currentDeviceType, MeshDeviceType.LightType newLightType) {
        if (!currentDeviceType.isSupportChangeDeviceType()) {
            Log.d("MeshCommand", "changeLightType command error, currentDeviceType doesn't support change light type function.");
            return requestMacDeviceType(address);
        }
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.param = 0x20;
        cmd.userData[0] = (byte) 0x00;
        cmd.userData[1] = (byte) 0x01;
        int newValue = MeshDeviceType.getLightTypeValue(newLightType) | (currentDeviceType.getRawValue2() & 0xF0);
        cmd.userData[2] = (byte) newValue;
        return cmd;
    }

    /// Reset light to IO Settings. You have to re-power the device after reset.
    public static MeshCommand resetLightType(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.param = 0x20;
        cmd.userData[0] = (byte) 0x00;
        cmd.userData[1] = (byte) 0xFF;
        return cmd;
    }

    /**
     * @param address
     * @param isOn
     * @param delay   Range [0, 0xFFFF], default is 0.
     * @return
     */
    public static MeshCommand turnOnOff(int address, Boolean isOn, int delay) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_ON_OFF;
        cmd.dst = address;
        cmd.param = isOn ? 0x01 : 0x00;
        cmd.userData[0] = (byte) (delay & 0xFF);
        cmd.userData[1] = (byte) ((delay >> 8) & 0xFF);
        return cmd;
    }

    public static MeshCommand turnOnOff(int address, Boolean isOn) {
        return MeshCommand.turnOnOff(address, isOn, 0);
    }

    /**
     * @param address
     * @param brightness Range [0, 100].
     * @return
     */
    public static MeshCommand setBrightness(int address, int brightness) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_BRIGHTNESS;
        cmd.dst = address;
        cmd.param = brightness;
        return cmd;
    }

    /**
     * @param address
     * @param value   Range [0, 100], 0 means the coolest color, 100 means the warmest color.
     * @return
     */
    public static MeshCommand setColorTemperature(int address, int value) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_SINGLE_CHANNEL;
        cmd.dst = address;
        cmd.param = Const.SINGLE_CHANNEL_COLOR_TEMPERATURE;
        cmd.userData[0] = (byte) (value & 0xFF);
        cmd.userData[1] = (byte) 0b0000_0000;
        return cmd;
    }

    /**
     * @param address
     * @param value   Range [0, 100].
     * @return
     */
    public static MeshCommand setWhitePercentage(int address, int value) {

        int currentValue = (int) ((float) value * 2.55);
        if (currentValue > 255) {
            currentValue = 255;
        }

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_SINGLE_CHANNEL;
        cmd.dst = address;
        cmd.param = Const.SINGLE_CHANNEL_COLOR_TEMPERATURE;
        cmd.userData[0] = (byte) (currentValue & 0xFF);
        cmd.userData[1] = (byte) 0b0001_0000;
        return cmd;
    }

    /**
     * @param address
     * @param value   Range [0, 255].
     * @return
     */
    public static MeshCommand setRed(int address, int value) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_SINGLE_CHANNEL;
        cmd.dst = address;
        cmd.param = Const.SINGLE_CHANNEL_RED;
        cmd.userData[0] = (byte) (value & 0xFF);
        return cmd;
    }

    /**
     * @param address
     * @param value   Range [0, 255].
     * @return
     */
    public static MeshCommand setGreen(int address, int value) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_SINGLE_CHANNEL;
        cmd.dst = address;
        cmd.param = Const.SINGLE_CHANNEL_GREEN;
        cmd.userData[0] = (byte) (value & 0xFF);
        return cmd;
    }

    /**
     * @param address
     * @param value   Range [0, 255].
     * @return
     */
    public static MeshCommand setBlue(int address, int value) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_SINGLE_CHANNEL;
        cmd.dst = address;
        cmd.param = Const.SINGLE_CHANNEL_BLUE;
        cmd.userData[0] = (byte) (value & 0xFF);
        return cmd;
    }

    /**
     * @param address
     * @param red     Range [0, 255].
     * @param green   Range [0, 255].
     * @param blue    Range [0, 255].
     * @return
     */
    public static MeshCommand setRgb(int address, int red, int green, int blue) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_SINGLE_CHANNEL;
        cmd.dst = address;
        cmd.param = Const.SINGLE_CHANNEL_RGB;
        cmd.userData[0] = (byte) (red & 0xFF);
        cmd.userData[1] = (byte) (green & 0xFF);
        cmd.userData[2] = (byte) (blue & 0xFF);
        return cmd;
    }

    public static MeshCommand syncDatetime(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_SYNC_DATETIME;
        cmd.dst = address;

        Calendar cal = Calendar.getInstance(Locale.getDefault());

        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH) + 1;
        int day = cal.get(Calendar.DAY_OF_MONTH);
        int hour = cal.get(Calendar.HOUR_OF_DAY);
        int minute = cal.get(Calendar.MINUTE);
        int second = cal.get(Calendar.SECOND);

        cmd.param = (year & 0xFF);
        cmd.userData[0] = (byte) ((year >> 8) & 0xFF);
        cmd.userData[1] = (byte) (month & 0xFF);
        cmd.userData[2] = (byte) (day & 0xFF);
        cmd.userData[3] = (byte) (hour & 0xFF);
        cmd.userData[4] = (byte) (minute & 0xFF);
        cmd.userData[5] = (byte) (second & 0xFF);
        return cmd;
    }

    public static MeshCommand getDatetime(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_GET_DATETIME;
        cmd.dst = address;
        cmd.param = 0x10;
        return cmd;
    }

    public static class GammaCurve {
        public static final byte gamma1_0 = (byte) 0x00;
        public static final byte gamma1_5 = (byte) 0x01;
        public static final byte gamma1_8 = (byte) 0x02;
        public static final byte gamma2_0 = (byte) 0x03;
        public static final byte gamma2_5 = (byte) 0x04;
        public static final byte gamma3_5 = (byte) 0x05;
        public static final byte gamma5_0 = (byte) 0x06;
    }

    public static MeshCommand setLightGammaCurve(int address, byte gamma) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.getUserData()[0] = Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.getUserData()[1] = Const.LIGHT_CONTROL_MODE_GAMMA_CURVE;
        cmd.getUserData()[2] = 0x01; // set
        cmd.getUserData()[3] = gamma;
        return cmd;
    }

    public static MeshCommand getLightGammaCurve(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.getUserData()[0] = Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.getUserData()[1] = Const.LIGHT_CONTROL_MODE_GAMMA_CURVE;
        cmd.getUserData()[2] = 0x00; // get
        return cmd;
    }

    /**
     * @param duration Range `[1, 0xFFFF]`, unit `seconds`.
     */
    public static MeshCommand setLightOnOffDuration(int address, int duration) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = Const.LIGHT_CONTROL_MODE_LIGHT_ON_OFF_DURATION;
        cmd.userData[2] = 0x01; // set
        cmd.userData[3] = (byte) (duration & 0xFF);
        cmd.userData[4] = (byte) ((duration >> 8) & 0xFF);
        return cmd;
    }

    public static MeshCommand getLightOnOffDuration(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = Const.LIGHT_CONTROL_MODE_LIGHT_ON_OFF_DURATION;
        cmd.userData[2] = 0x00; // get
        return cmd;
    }

    /**
     * @param address
     * @param lightSwitchType 0x01 - Normal ON OFF, 0x02 - Push Button, 0x03 - Three Channels
     * @return
     */
    public static MeshCommand setLightSwitchType(int address, int lightSwitchType) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = Const.SR_IDENTIFIER_LIGHT_SWITCH_TYPE;
        cmd.userData[1] = 0x01; // set
        cmd.userData[2] = (byte) (lightSwitchType & 0xFF);
        return cmd;
    }

    public static MeshCommand getLightSwitchType(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = Const.SR_IDENTIFIER_LIGHT_SWITCH_TYPE;
        cmd.userData[1] = 0x00; // get
        return cmd;
    }

    /**
     * @param address
     * @param frequency Range `[500, 10_000]`, unit `Hz`.
     * @return
     */
    public static MeshCommand setLightPwmFrequency(int address, int frequency) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = Const.LIGHT_CONTROL_MODE_LIGHT_PWM_FREQUENCY;
        cmd.userData[2] = 0x01; // set
        cmd.userData[3] = (byte) (frequency & 0xFF);
        cmd.userData[4] = (byte) ((frequency >> 8) & 0xFF);
        return cmd;
    }

    public static MeshCommand getLightPwmFrequency(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = Const.LIGHT_CONTROL_MODE_LIGHT_PWM_FREQUENCY;
        cmd.userData[2] = 0x00; // get
        return cmd;
    }

    /**
     * The device enters pairing mode for 5 seconds after receiving this command.
     */
    public static MeshCommand enablePairing(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = Const.SR_IDENTIFIER_SPECIAL;
        cmd.userData[1] = 0x01; //enable pairing
        return cmd;
    }

    /**
     * @param address
     * @param isEnabled If `true`, the other channels will be closed when change the RGB,
     *                  the RGB will be closed when change the other channels.
     * @return
     */
    public static MeshCommand setRgbIndependence(int address, boolean isEnabled) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = Const.LIGHT_CONTROL_MODE_CHANNEL_MODE;
        cmd.userData[2] = 0x04; // RGB independence
        cmd.userData[3] = 0x01; // set
        cmd.userData[4] = (byte) (isEnabled ? 0x01 : 0x00);
        return cmd;
    }

    public static MeshCommand getRgbIndependence(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = Const.LIGHT_CONTROL_MODE_CHANNEL_MODE;
        cmd.userData[2] = 0x04; // RGB independence
        cmd.userData[3] = 0x00; // get
        return cmd;
    }

    public static MeshCommand getPowerOnState(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = Const.LIGHT_CONTROL_MODE_POWER_ON_STATE;
        cmd.userData[2] = 0x00; // get
        return cmd;
    }

    public static MeshCommand setPowerOnState(int address, int level) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = Const.LIGHT_CONTROL_MODE_POWER_ON_STATE;
        cmd.userData[2] = 0x01; // set
        cmd.userData[3] = (byte) (level & 0xFF);
        return cmd;
    }

    public static MeshCommand getFirmwareVersion(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_GET_FIRMWARE;
        cmd.dst = address;
        cmd.param = 0x20;
        return cmd;
    }

    public static MeshCommand getGroups(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_GET_GROUPS;
        cmd.dst = address;
        cmd.param = 0x20;
        cmd.userData[0] = 0x01;
        return cmd;
    }

    public static MeshCommand getGroupDevices(int groupId) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_REPLACE_ADDRESS;
        cmd.dst = groupId;
        // !!! param and userData[0] must be 0xFF
        cmd.param = 0xFF;
        cmd.userData[0] = (byte) 0xFF;
        return cmd;
    }

    public static MeshCommand addGroup(int groupId, int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_GROUP_ACTION;
        cmd.dst = address;
        cmd.param = 0x01;
        cmd.userData[0] = (byte) (groupId & 0xFF);
        cmd.userData[1] = (byte) 0x80;
        return cmd;
    }

    public static MeshCommand deleteGroup(int groupId, int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_GROUP_ACTION;
        cmd.dst = address;
        cmd.param = 0x00;
        cmd.userData[0] = (byte) (groupId & 0xFF);
        cmd.userData[1] = (byte) 0x80;
        return cmd;
    }

    /**
     * @param address If you want to set a group address, such as `0x8001` means group 1.
     * @return
     */
    public static MeshCommand getGroupSyncInfo(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x11;
        cmd.userData[1] = (byte) 0x00;
        return cmd;
    }

    /// Add group and set as a sync master. address is light's address.
    public static MeshCommand addGroupSync(int groupId, int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_GROUP_ACTION;
        cmd.dst = address;
        cmd.param = 0x03;
        cmd.userData[0] = (byte) (groupId & 0xFF);
        cmd.userData[1] = (byte) 0x80;
        return cmd;
    }

    /// Remove the sync master only. address is light's address
    public static MeshCommand deleteGroupSync(int groupId, int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_GROUP_ACTION;
        cmd.dst = address;
        cmd.param = 0x02;
        cmd.userData[0] = (byte) (groupId & 0xFF);
        cmd.userData[1] = (byte) 0x80;
        return cmd;
    }

    public static MeshCommand getLightRunningMode(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = (byte) Const.LIGHT_CONTROL_MODE_GET_LIGHT_RUNNING_MODE;
        return cmd;
    }

    public static MeshCommand updateLightRunningMode(LightRunningMode mode) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = mode.address;
        cmd.userData[0] = (byte) Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = (byte) Const.LIGHT_CONTROL_MODE_SET_LIGHT_RUNNING_MODE;
        cmd.userData[2] = (byte) mode.state;

        switch (mode.state) {

            case LightRunningMode.State.DEFAULT_MODE:
                cmd.userData[3] = (byte) mode.defaultMode;
                break;

            case LightRunningMode.State.CUSTOM_MODE:
                cmd.userData[3] = (byte) mode.customModeId;
                cmd.userData[4] = (byte) mode.customMode;
                break;
        }

        return cmd;
    }

    /**
     * @param address
     * @param speed   range [0x00, 0x0F], 0x00 -> fastest, 0x0F -> slowest.
     */
    public static MeshCommand updateLightRunningSpeed(int address, int speed) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = (byte) Const.LIGHT_CONTROL_MODE_SET_LIGHT_RUNNING_SPEED;
        cmd.userData[2] = (byte) speed;
        return cmd;
    }

    public static MeshCommand getLightRunningCustomModeIdList(int address) {

        return getLightRunningCustomModeColors(address, 0x00);
    }

    /**
     * @param address
     * @param modeId  range [0x01, 0x10]
     */
    public static MeshCommand getLightRunningCustomModeColors(int address, int modeId) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = (byte) Const.LIGHT_CONTROL_MODE_CUSTOM_LIGHT_RUNNING_MODE;
        cmd.userData[2] = (byte) 0x00;
        cmd.userData[3] = (byte) modeId;
        return cmd;
    }

    public static ArrayList<MeshCommand> updateLightRunningCustomModeColors(int address, int modeId, ArrayList<LightRunningMode.Color> colors) {

        assert (modeId >= 0x01 && modeId <= 0x10);
        assert (colors.size() > 0 && colors.size() <= 5);

        ArrayList<MeshCommand> commands = new ArrayList<>();

        for (int i = 0; i < colors.size(); i++) {

            int index = i + 1;
            LightRunningMode.Color color = colors.get(i);

            MeshCommand cmd = new MeshCommand();
            cmd.tag = Const.TAG_APP_TO_NODE;
            cmd.dst = address;
            cmd.userData[0] = (byte) Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
            cmd.userData[1] = (byte) Const.LIGHT_CONTROL_MODE_CUSTOM_LIGHT_RUNNING_MODE;
            cmd.userData[2] = (byte) 0x01;
            cmd.userData[3] = (byte) modeId;
            cmd.userData[4] = (byte) index;
            cmd.userData[5] = (byte) color.red;
            cmd.userData[6] = (byte) color.green;
            cmd.userData[7] = (byte) color.blue;

            commands.add(cmd);
        }

        return commands;
    }

    public static MeshCommand removeLightRunningCustomModeId(int address, int modeId) {

        assert (modeId >= 0x01 && modeId <= 0x10);

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE;
        cmd.userData[1] = (byte) Const.LIGHT_CONTROL_MODE_CUSTOM_LIGHT_RUNNING_MODE;
        cmd.userData[2] = (byte) 0x02;
        cmd.userData[3] = (byte) modeId;

        return cmd;
    }

    public static MeshCommand setTimezone(int address, int hour, int minute, boolean isNegative) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        // The device won't send the timezone to other devices like date-time,
        // so I need to set timezone to 0xFFFF as a broadcast.
        // cmd.dst = address;
        cmd.dst = 0xFFFF;
        cmd.userData[0] = (byte) Const.SR_IDENTIFIER_TIMEZONE;
        cmd.userData[1] = 0x01; // set
        cmd.userData[2] = (byte) (Math.abs(hour) | (isNegative ? 0x80 : 0x00));
        cmd.userData[3] = (byte) minute;
        return cmd;
    }

    public static MeshCommand getTimezone(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) Const.SR_IDENTIFIER_TIMEZONE;
        cmd.userData[1] = 0x00; // get
        return cmd;
    }

    public static MeshCommand setLocation(int address, float longitude, float latitude) {

        byte[] longitudeData = HexUtil.getBytes(longitude);
        byte[] latitudeData = HexUtil.getBytes(latitude);

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) Const.SR_IDENTIFIER_SET_LOCATION;
        cmd.userData[1] = longitudeData[0];
        cmd.userData[2] = longitudeData[1];
        cmd.userData[3] = longitudeData[2];
        cmd.userData[4] = longitudeData[3];
        cmd.userData[5] = latitudeData[0];
        cmd.userData[6] = latitudeData[1];
        cmd.userData[7] = latitudeData[2];
        cmd.userData[8] = latitudeData[3];
        return cmd;
    }

    public static MeshCommand getLocation(int address) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) Const.SR_IDENTIFIER_GET_LOCATION;
        return cmd;
    }

    /**
     * @param address
     * @param type    MeshCommand.SunriseSunsetType
     * @return
     */
    public static MeshCommand getSunriseSunset(int address, byte type) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = type;
        return cmd;
    }

    public static MeshCommand setSunriseSunsetAction(int address, SunriseSunsetAction action) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = action.getType();
        int actionType = (int) (action.getActionType() & 0xFF);
        cmd.userData[1] = (byte) (actionType | (action.isEnabled ? 0x00 : 0x80));

        switch (action.getActionType()) {

            case SunriseSunsetActionType.ON_OFF:

                SunriseSunsetOnOffAction onOffAction = (SunriseSunsetOnOffAction) action;
                cmd.userData[2] = (byte) (onOffAction.isOn ? 0x01 : 0x00);
                cmd.userData[3] = 0x00;
                cmd.userData[4] = 0x00;
                cmd.userData[5] = 0x00;
                cmd.userData[6] = (byte) (onOffAction.duration & 0xFF);
                cmd.userData[7] = (byte) ((onOffAction.duration >> 8) & 0xFF);
                cmd.userData[8] = 0x00; // light endpoint bit, un-support now
                break;

            case SunriseSunsetActionType.SCENE:

                SunriseSunsetSceneAction sceneAction = (SunriseSunsetSceneAction) action;
                cmd.userData[2] = (byte) (sceneAction.sceneID & 0xFF);
                break;

            case SunriseSunsetActionType.CUSTOM:

                SunriseSunsetCustomAction customAction = (SunriseSunsetCustomAction) action;
                cmd.userData[2] = (byte) (customAction.brightness & 0xFF);
                cmd.userData[3] = (byte) (customAction.red & 0xFF);
                cmd.userData[4] = (byte) (customAction.green & 0xFF);
                cmd.userData[5] = (byte) (customAction.blue & 0xFF);
                cmd.userData[6] = (byte) (customAction.ctOrW & 0xFF);
                cmd.userData[7] = (byte) (customAction.duration & 0xFF);
                cmd.userData[8] = (byte) ((customAction.duration >> 8) & 0xFF);
                break;

            default:
                break;
        }

        return cmd;
    }

    /**
     * @param address
     * @param type    MeshCommand.SunriseSunsetType
     * @return
     */
    public static MeshCommand clearSunriseSunsetContent(int address, byte type) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = type;
        cmd.userData[1] = (byte) 0xC0; // clear
        return cmd;
    }

    /**
     * @param address
     * @param type      MeshCommand.SunriseSunsetType
     * @param isEnabled
     * @return
     */
    public static MeshCommand enableSunriseSunset(int address, byte type, boolean isEnabled) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = type;
        cmd.userData[1] = (byte) (isEnabled ? 0xE0 : 0xF0); // enable 0xE0, disable 0xF0
        return cmd;
    }

    public static MeshCommand addOrUpdateScene(int address, Scene scene, boolean isDoubleChannels) {
        if (isDoubleChannels) {
            return addOrUpdateDoubleChannelsScene(address, scene.sceneID, scene.isChannel1On(), scene.isChannel2On());
        }

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_SCENE;
        cmd.dst = address;
        cmd.param = 0x01; // add
        cmd.userData[0] = (byte) (scene.sceneID & 0xFF);
        cmd.userData[1] = (byte) (scene.brightness & 0xFF);
        cmd.userData[2] = (byte) (scene.red & 0xFF);
        cmd.userData[3] = (byte) (scene.green & 0xFF);
        cmd.userData[4] = (byte) (scene.blue & 0xFF);
        cmd.userData[5] = (byte) (scene.ctOrW & 0xFF);
        cmd.userData[6] = (byte) (scene.duration & 0xFF);
        cmd.userData[7] = (byte) ((scene.duration >> 8) & 0xFF);
        return cmd;
    }

    private static MeshCommand addOrUpdateDoubleChannelsScene(int address, int sceneID, boolean isChannel1On, boolean isChannel2On) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_SCENE;
        cmd.dst = address;
        cmd.param = 0x01; // add
        cmd.userData[0] = (byte) (sceneID & 0xFF);
        cmd.userData[1] = (byte) 0x01;
        cmd.userData[2] = 0x00;
        cmd.userData[3] = 0x00;
        cmd.userData[4] = 0x00;
        cmd.userData[5] = (byte) (isChannel1On ? 0x01 : 0x00);
        cmd.userData[6] = 0x00;
        cmd.userData[7] = 0x00;
        cmd.userData[8] = (byte) (isChannel2On ? 0x01 : 0x00);
        return cmd;
    }

    public static MeshCommand deleteScene(int address, int sceneID) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_SCENE;
        cmd.dst = address;
        cmd.param = 0x00; // delete
        cmd.userData[0] = (byte) (sceneID & 0xFF);
        return cmd;
    }

    public static MeshCommand clearScenes(int address) {

        return deleteScene(address, 0xFF);
    }

    public static MeshCommand loadScene(int address, int sceneID) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_LOAD_SCENE;
        cmd.dst = address;
        cmd.param = (byte) sceneID & 0xFF;
        return cmd;
    }

    public static MeshCommand getSceneDetail(int address, int sceneID) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_GET_SCENE;
        cmd.dst = address;
        cmd.param = 0x20;
        cmd.userData[0] = (byte) (sceneID & 0xFF);
        return cmd;
    }

    public static AbstractAlarm makeAlarm(MeshCommand command) {

        // 0xA5 is valid alarm
        if (command.getParam() != 0xA5) return null;
        int alarmID = (int) command.userData[0] & 0xFF;
        if (alarmID < 1 || alarmID > 16) return null;

        int event = (int) (command.userData[1] & 0xFF);
        // bit0~bit3, 0 off, 1 on, 2 scene
        int actionType = event & 0b1111;
        int dayType = (event & 0b0111_0000) >> 4;

        boolean isEnabled = (event & 0x80) == 0x80;
        int hour = (int) (command.userData[4] & 0xFF);
        int minute = (int) (command.userData[5] & 0xFF);
        int second = (int) (command.userData[6] & 0xFF);
        int sceneID = (int) (command.userData[7] & 0xFF);

        if (dayType == AlarmDayType.DAY) {

            int month = (int) (command.userData[2] & 0xFF);
            if (month < 1 || month > 12) return null;

            int day = (int) (command.userData[3] & 0xFF);

            DayAlarm dayAlarm = new DayAlarm(alarmID);
            dayAlarm.setActionType(actionType);
            dayAlarm.setEnabled(isEnabled);
            dayAlarm.setHour(hour);
            dayAlarm.setMinute(minute);
            dayAlarm.setSecond(second);
            dayAlarm.setSceneID(sceneID);
            dayAlarm.setMonth(month);
            dayAlarm.setDay(day);

            return dayAlarm;

        } else if (dayType == AlarmDayType.WEEK) {

            int week = (int) (command.userData[3] & 0xFF) & 0x7F;

            WeekAlarm weekAlarm = new WeekAlarm(alarmID);
            weekAlarm.setActionType(actionType);
            weekAlarm.setEnabled(isEnabled);
            weekAlarm.setHour(hour);
            weekAlarm.setMinute(minute);
            weekAlarm.setSecond(second);
            weekAlarm.setSceneID(sceneID);
            weekAlarm.setWeek(week);

            return weekAlarm;
        }

        return null;
    }

    /**
     * @param address
     * @param alarmID The `alarmID = 0` means get all alarms of the device.
     * @return
     */
    public static MeshCommand getAlarm(int address, int alarmID) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_GET_ALARM;
        cmd.dst = address;
        cmd.userData[0] = (byte) alarmID;
        return cmd;
    }

    /**
     * Note: `alarm.alarmID` will be set to `0x00`, the device will automatically set the new `alarmID`.
     *
     * @param address
     * @param alarm
     * @return
     */
    public static MeshCommand addAlarm(int address, AbstractAlarm alarm) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_EDIT_ALARM;
        cmd.dst = address;
        cmd.param = 0x00; // add
        cmd.userData[0] = 0x00; // automatically set alarmID
        cmd.userData[1] = (byte) alarm.getAlarmEvent();

        // 2 day.month
        // 3 day.day, week.week
        if (alarm.getDayType() == AlarmDayType.DAY) {

            DayAlarm dayAlarm = (DayAlarm) alarm;
            cmd.userData[2] = (byte) (dayAlarm.month);
            cmd.userData[3] = (byte) (dayAlarm.day);

        } else if (alarm.getDayType() == AlarmDayType.WEEK) {

            WeekAlarm weekAlarm = (WeekAlarm) alarm;
            cmd.userData[3] = (byte) (weekAlarm.week & 0x7F);
        }

        cmd.userData[4] = (byte) (alarm.hour);
        cmd.userData[5] = (byte) (alarm.minute);
        cmd.userData[6] = (byte) (alarm.second);
        cmd.userData[7] = (byte) (alarm.sceneID);
        return cmd;
    }

    public static MeshCommand enableAlarm(int address, int alarmID, boolean isEnabled) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_EDIT_ALARM;
        cmd.dst = address;
        // enable 0x03, disable 0x04
        cmd.param = isEnabled ? 0x03 : 0x04;
        cmd.userData[0] = (byte) alarmID;
        return cmd;
    }

    public static MeshCommand deleteAlarm(int address, int alarmID) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_EDIT_ALARM;
        cmd.dst = address;
        cmd.param = 0x01; // delete
        cmd.userData[0] = (byte) alarmID;
        return cmd;
    }

    public static MeshCommand updateAlarm(int address, AbstractAlarm alarm) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_EDIT_ALARM;
        cmd.dst = address;
        cmd.param = 0x02; // update
        cmd.userData[0] = (byte) (alarm.alarmID);
        cmd.userData[1] = (byte) (alarm.getAlarmEvent());

        // 2 day.month
        // 3 day.day, week.week
        if (alarm.getDayType() == AlarmDayType.DAY) {

            DayAlarm dayAlarm = (DayAlarm) alarm;
            cmd.userData[2] = (byte) (dayAlarm.month);
            cmd.userData[3] = (byte) (dayAlarm.day);

        } else if (alarm.getDayType() == AlarmDayType.WEEK) {

            WeekAlarm weekAlarm = (WeekAlarm) alarm;
            cmd.userData[3] = (byte) (weekAlarm.week & 0x7F);
        }

        cmd.userData[4] = (byte) (alarm.hour);
        cmd.userData[5] = (byte) (alarm.minute);
        cmd.userData[6] = (byte) (alarm.second);
        cmd.userData[7] = (byte) (alarm.sceneID);
        return cmd;
    }

    public static MeshCommand getSmartSwitchSecretKey(int mode, int groupId) {

        long switchId = getSmartSwitchIdWithGroupId(groupId);

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = Address.connectNode;
        cmd.userData[0] = (byte) 0x38;
        cmd.userData[1] = (byte) 0x01; // start
        cmd.userData[2] = (byte) (mode);
        cmd.userData[3] = (byte) (switchId & 0xFF);
        cmd.userData[4] = (byte) ((switchId >> 8) & 0xFF);
        cmd.userData[5] = (byte) ((switchId >> 16) & 0xFF);
        cmd.userData[6] = (byte) ((switchId >> 24) & 0xFF);
        return cmd;
    }

    /**
     * @param address Device address
     * @param index   Switch index, range [0, 7]
     * @return
     */
    public static MeshCommand getSmartSwitchId(int address, int index) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x72;
        cmd.userData[1] = (byte) (index);
        return cmd;
    }

    /**
     * @param address Device address
     * @param groupId Group ID (Room ID)
     * @return
     */
    public static MeshCommand addSmartSwitchIdWithGroupId(int address, int groupId) {

        long switchId = getSmartSwitchIdWithGroupId(groupId);

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x03;
        cmd.userData[2] = (byte) 0x01;
        cmd.userData[3] = (byte) 0x01;
        cmd.userData[4] = (byte) ((switchId >> 24) & 0xFF);
        cmd.userData[5] = (byte) ((switchId >> 16) & 0xFF);
        cmd.userData[6] = (byte) ((switchId >> 8) & 0xFF);
        cmd.userData[7] = (byte) ((switchId) & 0xFF);
        return cmd;
    }

    /**
     * Delete the smart switch ID in the group.
     *
     * @param address Device address
     * @param groupId Group ID
     * @return
     */
    public static MeshCommand deleteSmartSwitchIdWithGroupId(int address, int groupId) {

        long switchId = getSmartSwitchIdWithGroupId(groupId);

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x03;
        cmd.userData[2] = (byte) 0x02;
        cmd.userData[3] = (byte) 0x01;
        cmd.userData[4] = (byte) ((switchId >> 24) & 0xFF);
        cmd.userData[5] = (byte) ((switchId >> 16) & 0xFF);
        cmd.userData[6] = (byte) ((switchId >> 8) & 0xFF);
        cmd.userData[7] = (byte) ((switchId) & 0xFF);
        return cmd;
    }

    /**
     * Delete the switch ID.
     *
     * @param address  Device address
     * @param switchId Switch ID, for example: 0x80010001
     * @return
     */
    public static MeshCommand deleteSmartSwitchId(int address, long switchId) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x03;
        cmd.userData[2] = (byte) 0x02;
        cmd.userData[3] = (byte) 0x01;
        cmd.userData[4] = (byte) ((switchId >> 24) & 0xFF);
        cmd.userData[5] = (byte) ((switchId >> 16) & 0xFF);
        cmd.userData[6] = (byte) ((switchId >> 8) & 0xFF);
        cmd.userData[7] = (byte) ((switchId) & 0xFF);
        return cmd;
    }

    /**
     * Get Sensor Attribute Value.
     *
     * @param address Device address.
     * @param type    Such as SensorAttributeType.HUMAN_INDUCTION_SENSITIVITY
     * @return
     */
    public static MeshCommand getSensorAttribute(int address, int type) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x09;
        cmd.userData[1] = (byte) 0x00; // get
        cmd.userData[2] = (byte) type;
        return cmd;
    }

    /**
     * Update Sensor Attribute Value.
     *
     * @param address Device address.
     * @param type    Such as SensorAttributeType.HUMAN_INDUCTION_SENSITIVITY
     * @param value   New Attribute Value.
     * @return
     */
    public static MeshCommand setSensorAttribute(int address, int type, int value) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x09;
        cmd.userData[1] = (byte) 0x01; // set
        cmd.userData[2] = (byte) type;

        if (value < 0 && type == SensorAttributeType.LUX_ZERO_DEVIATION_OF_THE_BRIGHTNESS_SENSOR) {

            int newValue = ((value + 128) | 0x80) & 0xFF;
            cmd.userData[3] = (byte) newValue;

        } else {

            cmd.userData[3] = (byte) (value & 0xFF);
            cmd.userData[4] = (byte) ((value >> 8) & 0xFF);
        }

        return cmd;
    }

    public static class SensorType {
        public static final byte RESERVED = 0x00;
        public static final byte DOOR_SENSOR = 0x01;
        public static final byte PIR_MOTION = 0x02;
        public static final byte MICROWAVE_MOTION = 0x03;
        public static final byte LUX = 0x04;
        public static final byte TEMPERATURE = 0x05;
    }

    /// - parameters:
    ///     - address: Device address, target address, add sensor events for it.
    ///     - sensorType: Sensor type.
    ///     - sensorId: For example, the Mac of the device is `AA679C000001`, the sensorId is 0x9C000001, the last 4 bytes of the Mac.
    public static MeshCommand bindSensorId(int address, byte sensorType, long sensorId) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = 0x12;
        cmd.userData[1] = 0x04; // bind sensor
        cmd.userData[2] = 0x01; // bind
        cmd.userData[3] = sensorType;
        cmd.userData[4] = (byte) ((sensorId >> 24) & 0xFF);
        cmd.userData[5] = (byte) ((sensorId >> 16) & 0xFF);
        cmd.userData[6] = (byte) ((sensorId >> 8) & 0xFF);
        cmd.userData[7] = (byte) (sensorId & 0xFF);
        return cmd;
    }

    public static MeshCommand unbindSensorId(int address, byte sensorType) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = 0x12;
        cmd.userData[1] = 0x04; // bind sensor
        cmd.userData[2] = 0x02; // unbind
        cmd.userData[3] = sensorType;
        return cmd;
    }

    // SensorType.DOOR_SENSOR
    public static MeshCommand getSensorId(int address, byte sensorType) {

        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = 0x12;
        cmd.userData[1] = 0x04; // bind sensor
        cmd.userData[2] = 0x00; // get
        cmd.userData[3] = sensorType;
        return cmd;
    }

    public static MeshCommand identify(int address) {
        return unbindSensorId(address, SensorType.MICROWAVE_MOTION);
    }

    // sensorEvent: SensorEvent.DOOR_OPEN, SensorEvent.DOOR_CLOSED, etc.
    public static MeshCommand getSensorAction(int address, int sensorEvent) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) (sensorEvent & 0xFF);
        cmd.userData[1] = 0x00; // get
        return cmd;
    }

    public static MeshCommand setSensorAction(int address, int sensorEvent, SensorAction action) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) (sensorEvent & 0xFF);
        cmd.userData[1] = action.isEnabled ? action.getCommandCode() : (byte) (action.getCommandCode() | 0x80);

        switch (action.uniqueId) {
            case SensorAction.UNIQUE_ID_TURN_ON_OFF: {
                cmd.userData[2] = action.isOn ? (byte) 0x01 : (byte) 0x00;
                cmd.userData[6] = (byte) (action.transition & 0xFF);
                cmd.userData[7] = (byte) ((action.transition >> 8) & 0xFF);
                break;
            }
            case SensorAction.UNIQUE_ID_RECALL_SCENE: {
                cmd.userData[2] = (byte) (action.sceneId & 0xFF);
                break;
            }
            case SensorAction.UNIQUE_ID_SET_STATE: {
                cmd.userData[2] = (byte) (action.brightness & 0xFF);
                cmd.userData[3] = (byte) (action.red & 0xFF);
                cmd.userData[4] = (byte) (action.green & 0xFF);
                cmd.userData[5] = (byte) (action.blue & 0xFF);
                cmd.userData[6] = (byte) (action.ctOrWhite & 0xFF);
                cmd.userData[7] = (byte) (action.transition & 0xFF);
                cmd.userData[8] = (byte) ((action.transition >> 8) & 0xFF);
                break;
            }
            case SensorAction.UNIQUE_ID_SET_BRIGHTNESS: {
                cmd.userData[2] = (byte) (action.brightness & 0xFF);
                cmd.userData[6] = (byte) (action.transition & 0xFF);
                cmd.userData[7] = (byte) ((action.transition >> 8) & 0xFF);
                break;
            }
            case SensorAction.UNIQUE_ID_SET_RGB: {
                cmd.userData[2] = 0x04;
                cmd.userData[3] = (byte) (action.red & 0xFF);
                cmd.userData[4] = (byte) (action.green & 0xFF);
                cmd.userData[5] = (byte) (action.blue & 0xFF);
                break;
            }
            case SensorAction.UNIQUE_ID_SET_RED: {
                cmd.userData[2] = 0x01;
                cmd.userData[3] = (byte) (action.red & 0xFF);
                break;
            }
            case SensorAction.UNIQUE_ID_SET_GREEN: {
                cmd.userData[2] = 0x02;
                cmd.userData[3] = (byte) (action.green & 0xFF);
                break;
            }
            case SensorAction.UNIQUE_ID_SET_BLUE: {
                cmd.userData[2] = 0x03;
                cmd.userData[3] = (byte) (action.blue & 0xFF);
                break;
            }
            case SensorAction.UNIQUE_ID_SET_CT_OR_WHITE: {
                cmd.userData[2] = 0x05;
                cmd.userData[3] = (byte) (action.ctOrWhite & 0xFF);
                break;
            }
            case SensorAction.UNIQUE_ID_SET_RUNNING: {
                cmd.userData[2] = 0x05;
                cmd.userData[3] = 0x01;
                cmd.userData[4] = (byte) (action.runningIndex & 0xFF);
                break;
            }
            case SensorAction.UNIQUE_ID_SET_CUSTOM_RUNNING: {
                cmd.userData[2] = 0x05;
                cmd.userData[3] = 0x02;
                cmd.userData[4] = (byte) (action.customRunningIndex & 0xFF);
                cmd.userData[5] = (byte) action.customRunningMode;
                break;
            }
            case SensorAction.UNIQUE_ID_STOP_RUNNING: {
                break;
            }
            case SensorAction.UNIQUE_ID_NONE: {
                cmd.userData[1] = action.getCommandCode();
                break;
            }
        }
        return cmd;
    }

    public static class SensorAction {
        private boolean isEnabled = true;
        private boolean isOn = true;
        private int sceneId = 0;
        private int brightness = 100;
        private int red = 255;
        private int green = 255;
        private int blue = 255;
        private int ctOrWhite = 100;
        private int transition = 0;
        private int runningIndex = 1;
        private int customRunningIndex = 1;
        private int customRunningMode = 0;// default is none

        public byte getCommandCode() {
            switch (uniqueId) {
                case UNIQUE_ID_TURN_ON_OFF:
                    return TURN_ON_OFF;
                case UNIQUE_ID_RECALL_SCENE:
                    return RECALL_SCENE;
                case UNIQUE_ID_SET_STATE:
                    return SET_STATE;
                case UNIQUE_ID_SET_BRIGHTNESS:
                    return SET_BRIGHTNESS;
                case UNIQUE_ID_SET_RED:
                case UNIQUE_ID_SET_GREEN:
                case UNIQUE_ID_SET_BLUE:
                case UNIQUE_ID_SET_RGB:
                case UNIQUE_ID_SET_CT_OR_WHITE:
                    return SET_RGB_CT_OR_WHITE;
                case UNIQUE_ID_SET_RUNNING:
                case UNIQUE_ID_SET_CUSTOM_RUNNING:
                case UNIQUE_ID_STOP_RUNNING:
                    return SET_RUNNING;
                default:
                    return NONE;
            }
        }

        // CommandCode
        public static final byte TURN_ON_OFF = 0x01;
        public static final byte RECALL_SCENE = 0x02;
        public static final byte SET_STATE = 0x04;
        public static final byte SET_BRIGHTNESS = 0x05;
        public static final byte SET_RGB_CT_OR_WHITE = 0x06;
        public static final byte SET_RUNNING = 0x07;
        public static final byte NONE = (byte) 0xC0;

        private int uniqueId = 1; // default is turnOnOff 1

        public static final byte UNIQUE_ID_TURN_ON_OFF = 1;
        public static final byte UNIQUE_ID_RECALL_SCENE = 2;
        public static final byte UNIQUE_ID_SET_STATE = 3;
        public static final byte UNIQUE_ID_SET_BRIGHTNESS = 4;
        public static final byte UNIQUE_ID_SET_RGB = 5;
        public static final byte UNIQUE_ID_SET_RED = 6;
        public static final byte UNIQUE_ID_SET_GREEN = 7;
        public static final byte UNIQUE_ID_SET_BLUE = 8;
        public static final byte UNIQUE_ID_SET_CT_OR_WHITE = 9;
        public static final byte UNIQUE_ID_SET_RUNNING = 10;
        public static final byte UNIQUE_ID_SET_CUSTOM_RUNNING = 11;
        public static final byte UNIQUE_ID_STOP_RUNNING = 12;
        public static final byte UNIQUE_ID_NONE = 13;

        public boolean isEnabled() {
            return isEnabled;
        }

        public boolean isOn() {
            return isOn;
        }

        public int getSceneId() {
            return sceneId;
        }

        public int getBrightness() {
            return brightness;
        }

        public int getRed() {
            return red;
        }

        public int getGreen() {
            return green;
        }

        public int getBlue() {
            return blue;
        }

        public int getCtOrWhite() {
            return ctOrWhite;
        }

        public int getTransition() {
            return transition;
        }

        public int getRunningIndex() {
            return runningIndex;
        }

        public int getCustomRunningIndex() {
            return customRunningIndex;
        }

        public int getCustomRunningMode() {
            return customRunningMode;
        }

        public int getUniqueId() {
            return uniqueId;
        }

        public void setEnabled(boolean enabled) {
            isEnabled = enabled;
        }

        public void setOn(boolean on) {
            isOn = on;
        }

        // sceneId value range [1, 254]
        public void setSceneId(int sceneId) {
            this.sceneId = sceneId;
        }

        // brightness range [0, 100]
        public void setBrightness(int brightness) {
            this.brightness = brightness;
        }

        // red range [0, 255]
        public void setRed(int red) {
            this.red = red;
        }

        // green range [0, 255]
        public void setGreen(int green) {
            this.green = green;
        }

        // blue range [0, 255]
        public void setBlue(int blue) {
            this.blue = blue;
        }

        // ct range [0, 100], white range [0, 255]
        public void setCtOrWhite(int ctOrWhite) {
            this.ctOrWhite = ctOrWhite;
        }

        // transition value range [0, 65535] seconds.
        public void setTransition(int transition) {
            this.transition = transition;
        }

        // index range [1, 20]
        public void setRunningIndex(int runningIndex) {
            this.runningIndex = runningIndex;
        }

        // index range [0x01, 0x10]
        public void setCustomRunningIndex(int customRunningIndex) {
            this.customRunningIndex = customRunningIndex;
        }

        public void setCustomRunningMode(int customRunningMode) {
            this.customRunningMode = customRunningMode;
        }

        private SensorAction() {
        }

        public static SensorAction makeOnOff(boolean isOn) {
            SensorAction action = new SensorAction();
            action.uniqueId = UNIQUE_ID_TURN_ON_OFF;
            action.isOn = isOn;
            return action;
        }

        // sceneId value range [1, 254]
        public static SensorAction makeRecallScene(int sceneId) {
            SensorAction action = new SensorAction();
            action.uniqueId = UNIQUE_ID_RECALL_SCENE;
            action.sceneId = sceneId;
            return action;
        }

        // red, green, blue, white range [0, 255], brightness, ct range [0, 100]
        public static SensorAction makeSetState(int brightness, int red, int green, int blue, int ctOrWhite) {
            SensorAction action = new SensorAction();
            action.uniqueId = UNIQUE_ID_SET_STATE;
            action.brightness = brightness;
            action.red = red;
            action.green = green;
            action.blue = blue;
            action.ctOrWhite = ctOrWhite;
            return action;
        }

        // brightness range [0, 100]
        public static SensorAction makeSetBrightness(int brightness) {
            SensorAction action = new SensorAction();
            action.uniqueId = UNIQUE_ID_SET_BRIGHTNESS;
            action.brightness = brightness;
            return action;
        }

        // red, green, blue range [0, 255]
        public static SensorAction makeSetRgb(int red, int green, int blue) {
            SensorAction action = new SensorAction();
            action.uniqueId = UNIQUE_ID_SET_RGB;
            action.red = red;
            action.green = green;
            action.blue = blue;
            return action;
        }

        // red range [0, 255]
        public static SensorAction makeSetRed(int red) {
            SensorAction action = new SensorAction();
            action.uniqueId = UNIQUE_ID_SET_RED;
            action.red = red;
            return action;
        }

        // green range [0, 255]
        public static SensorAction makeSetGreen(int green) {
            SensorAction action = new SensorAction();
            action.uniqueId = UNIQUE_ID_SET_GREEN;
            action.green = green;
            return action;
        }

        // blue range [0, 255]
        public static SensorAction makeSetBlue(int blue) {
            SensorAction action = new SensorAction();
            action.uniqueId = UNIQUE_ID_SET_BLUE;
            action.blue = blue;
            return action;
        }

        // white range [0, 255], ct range [0, 100]
        public static SensorAction makeSetCtOrWhite(int ctOrWhite) {
            SensorAction action = new SensorAction();
            action.uniqueId = UNIQUE_ID_SET_CT_OR_WHITE;
            action.ctOrWhite = ctOrWhite;
            return action;
        }

        // none equals `Undefined`
        public static SensorAction makeNone() {
            SensorAction action = new SensorAction();
            action.uniqueId = UNIQUE_ID_NONE;
            return action;
        }

    }

    private static long getSmartSwitchIdWithGroupId(int groupId) {

        long value = (long) groupId << 16;
        long trailing = 0x0001;
        return value | trailing;
    }

    private int increaseSeqNo() {

        seqNo += 1;
        if (seqNo >= 0xFFFFFF) {
            seqNo = 1;
        }

        return seqNo;
    }

    public byte[] getCommandData() {

        byte[] data = new byte[20];

        int seqNo = this.increaseSeqNo();
        data[0] = (byte) ((seqNo >> 16) & 0xFF);
        data[1] = (byte) ((seqNo >> 8) & 0xFF);
        data[2] = (byte) ((seqNo) & 0xFF);

        data[3] = (byte) (src & 0xFF);
        data[4] = (byte) ((src >> 8) & 0xFF);
        data[5] = (byte) (dst & 0xFF);
        data[6] = (byte) ((dst >> 8) & 0xFF);

        data[7] = (byte) tag;
        data[8] = (byte) ((vendorId >> 8) & 0xFF);
        data[9] = (byte) (vendorId & 0xFF);
        data[10] = (byte) (param);

        for (int i = 0; i < 9; i++) {
            int dataIndex = 11 + i;
            data[dataIndex] = userData[i];
        }

        return data;
    }

    public int getSeqNo() {
        return seqNo;
    }

    public int getSrc() {
        return src;
    }

    public int getDst() {
        return dst;
    }

    public int getTag() {
        return tag;
    }

    public int getVendorId() {
        return vendorId;
    }

    public int getParam() {
        return param;
    }

    public byte[] getUserData() {
        return userData;
    }

    public static final class Address {

        public static final int all = 0xFFFF;

        public static final int connectNode = 0x0000;
    }

    static final class Const {

        static final int TAG_APP_TO_NODE = 0xEA;

        static final int TAG_NODE_TO_APP = 0xEB;

        static final int TAG_GET_STATUS = 0xDA;
        static final int TAG_RESPONSE_STATE = 0xDB;

        static final int TAG_LIGHT_STATUS = 0xDC;

        static final int TAG_ON_OFF = 0xD0;

        static final int TAG_BRIGHTNESS = 0xD2;

        static final int TAG_SINGLE_CHANNEL = 0xE2;

        static final int TAG_REPLACE_ADDRESS = 0xE0;

        static final int TAG_DEVICE_ADDRESS_NOTIFY = 0xE1;

        static final int TAG_RESET_NETWORK = 0xE3;

        static final int TAG_SYNC_DATETIME = 0xE4;

        static final int TAG_GET_DATETIME = 0xE8;

        static final int TAG_DATETIME_RESPONSE = 0xE9;

        static final int TAG_GET_FIRMWARE = 0xC7;

        static final int TAG_FIRMWARE_RESPONSE = 0xC8;

        static final int TAG_GET_GROUPS = 0xDD;

        static final int TAG_RESPONSE_GROUPS = 0xD4;

        static final int TAG_GROUP_ACTION = 0xD7;

        static final int TAG_SCENE = 0xEE;

        static final int TAG_LOAD_SCENE = 0xEF;

        static final int TAG_GET_SCENE = 0xC0;

        static final int TAG_GET_SCENE_RESPONSE = 0xC1;

        static final int TAG_EDIT_ALARM = 0xE5;

        static final int TAG_GET_ALARM = 0xE6;

        static final int TAG_GET_ALARM_RESPONSE = 0xE7;

        static final int TAG_UART_MODULE = 0XFD;


        static final int SR_IDENTIFIER_MAC = 0x76;


        static final int SINGLE_CHANNEL_RED = 0x01;

        static final int SINGLE_CHANNEL_GREEN = 0x02;

        static final int SINGLE_CHANNEL_BLUE = 0x03;

        static final int SINGLE_CHANNEL_RGB = 0x04;

        static final int SINGLE_CHANNEL_COLOR_TEMPERATURE = 0x05;

        static final int SINGLE_CHANNEL_W1 = 0x06;

        static final int SINGLE_CHANNEL_W2 = 0x07;

        static final int SINGLE_CHANNEL_W3 = 0x08;

        static final int SINGLE_CHANNEL_WWW = 0x09;

        static final int SINGLE_CHANNEL_RGB_WWW = 0x0A;

        static final int SR_IDENTIFIER_LIGHT_CONTROL_MODE = 0x01;

        static final int LIGHT_CONTROL_MODE_GAMMA_CURVE = 0x0E;

        static final int LIGHT_CONTROL_MODE_LIGHT_ON_OFF_DURATION = 0x0F;

        static final int LIGHT_CONTROL_MODE_GET_LIGHT_RUNNING_MODE = 0x00;

        static final int LIGHT_CONTROL_MODE_SET_LIGHT_RUNNING_MODE = 0x05;

        static final int LIGHT_CONTROL_MODE_SET_LIGHT_RUNNING_SPEED = 0x03;

        static final int LIGHT_CONTROL_MODE_CUSTOM_LIGHT_RUNNING_MODE = 0x01;

        static final int LIGHT_CONTROL_MODE_LIGHT_PWM_FREQUENCY = 0x0A;

        static final int LIGHT_CONTROL_MODE_CHANNEL_MODE = 0x07;

        static final int LIGHT_CONTROL_MODE_POWER_ON_STATE = 0x10;


        static final int SR_IDENTIFIER_LIGHT_SWITCH_TYPE = 0x07;

        static final int LIGHT_SWITCH_TYPE_NORMAL_ON_OFF = 0x01;

        static final int LIGHT_SWITCH_TYPE_PUSH_BUTTON = 0x02;

        static final int LIGHT_SWITCH_TYPE_THREE_CHANNELS = 0x03;


        static final int SR_IDENTIFIER_SPECIAL = 0x12;

        static final int SR_IDENTIFIER_TIMEZONE = 0x1E;

        static final int SR_IDENTIFIER_SET_LOCATION = 0x1A;
        static final int SR_IDENTIFIER_GET_LOCATION = 0x1B;

        static final int SR_IDENTIFIER_SUNRISE = 0x1C;
        static final int SR_IDENTIFIER_SUNSET = 0x1D;

        static final int SR_IDENTIFIER_SYNC_INFO = 0x11;

        static final int SR_IDENTIFIER_SMART_SWITCH_ID = 0x72;

        static final int SR_IDENTIFIER_SENSOR_REPORT = 0x05;

        /// AT5810S Uart Tx BL9032A-MW-20210322
        static final int SR_IDENTIFIER_SENSOR_UART_TX = 0x09;

        static final int SR_IDENTIFIER_DOOR_SENSOR_OPEN = 0x41;

        static final int SR_IDENTIFIER_DOOR_SENSOR_CLOSED = 0x51;

        static final int SR_IDENTIFIER_PIR_DETECTED = 0x42;

        static final int SR_IDENTIFIER_PIR_NOT_DETECTED = 0x52;

        static final int SR_IDENTIFIER_MICROWAVE_DETECTED = 0x43;

        static final int SR_IDENTIFIER_MICROWAVE_NOT_DETECTED = 0x53;

        static final int WATER_LEAK_DETECTED = 0x47;
        static final int WATER_LEAK_NOT_DETECTED = 0x57;
        static final int SMOKE_DETECTED = 0x48;
        static final int SMOKE_NOT_DETECTED = 0x58;
        static final int CO_DETECTED = 0x49;
        static final int CO_NOT_DETECTED = 0x59;
        static final int GAS_DETECTED = 0x4A;
        static final int GAS_NOT_DETECTED = 0x5A;
        static final int AIR_QUALITY_DETECTED = 0x4B;
        static final int AIR_QUALITY_NOT_DETECTED = 0x5B;
        static final int GLASS_BREAK_DETECTED = 0x4C;
        static final int GLASS_BREAK_NOT_DETECTED = 0x5C;
        static final int VIBRATION_DETECTED = 0x4D;
        static final int VIBRATION_NOT_DETECTED = 0x5D;
        static final int MULTI_SENSOR_ACTION = 0x19;
        static final int CURTAIN_REPORT = 0x25;

        static final int SR_IDENTIFIER_UNIVERSAL_REMOTE = 0x16;

        static final int SR_IDENTIFIER_PWM_CHANNELS_STATUS = 0x78;
    }

    public static class LightRunningMode {

        private int address;

        private int state = State.STOPPED;

        /**
         * range [0x01, 0x14]
         */
        private int defaultMode = DefaultMode.COLORFUL_MIXED;

        /**
         * range [0x01, 0x06]
         */
        private int customMode = CustomMode.ASCEND_SHADE;

        /**
         * range [0x00, 0x0F]
         */
        private int speed = 0x0A;

        /**
         * range [0x01, 0x10]
         */
        private int customModeId = 0x01;

        private LightRunningMode() {

        }

        public LightRunningMode(int address, int state) {
            this.address = address;
            this.state = state;
        }

        static LightRunningMode makeWithUserData(int address, byte[] userData) {

            if (((int) userData[0] & 0xFF) != Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE) return null;
            if (((int) userData[1] & 0xFF) != Const.LIGHT_CONTROL_MODE_GET_LIGHT_RUNNING_MODE)
                return null;

            int state = (int) userData[4] & 0xFF;
            if (state > 0x02) return null;

            LightRunningMode lightRunningMode = new LightRunningMode(address, state);
            lightRunningMode.speed = Math.max(0x00, Math.min(0x0F, (int) userData[2] & 0xFF));

            switch (state) {

                case State.STOPPED:
                    break;

                case State.DEFAULT_MODE:
                    lightRunningMode.defaultMode = (int) userData[5] & 0xFF;
                    break;

                case State.CUSTOM_MODE:
                    lightRunningMode.customModeId = Math.max(0x01, Math.min(0x10, (int) userData[5] & 0xFF));
                    lightRunningMode.customMode = (int) userData[6] & 0xFF;
                    break;
            }

            return lightRunningMode;
        }

        public int getAddress() {
            return address;
        }

        public void setAddress(int address) {
            this.address = address;
        }

        public int getState() {
            return state;
        }

        public void setState(int state) {
            this.state = state;
        }

        public int getDefaultMode() {
            return defaultMode;
        }

        public void setDefaultMode(int defaultMode) {
            this.defaultMode = defaultMode;
        }

        public int getCustomMode() {
            return customMode;
        }

        public void setCustomMode(int customMode) {
            this.customMode = customMode;
        }

        public int getSpeed() {
            return speed;
        }

        public void setSpeed(int speed) {
            this.speed = speed;
        }

        public int getCustomModeId() {
            return customModeId;
        }

        public void setCustomModeId(int customModeId) {
            this.customModeId = customModeId;
        }

        public static final class State {

            public static final int STOPPED = 0x00;
            public static final int DEFAULT_MODE = 0x01;
            public static final int CUSTOM_MODE = 0x02;
        }

        public static final class DefaultMode {

            public static final int COLORFUL_MIXED = 0x01;
            public static final int RED_SHADE = 0x02;
            public static final int GREEN_SHADE = 0x03;
            public static final int BLUE_SHADE = 0x04;
            public static final int YELLOW_SHADE = 0x05;
            public static final int CYAN_SHADE = 0x06;
            public static final int PURPLE_SHADE = 0x07;
            public static final int WHITE_SHADE = 0x08;
            public static final int RED_GREEN_SHADE = 0x09;
            public static final int RED_BLUE_SHADE = 0x0A;
            public static final int GREEN_BLUE_SHADE = 0x0B;
            public static final int COLORFUL_STROBE = 0x0C;
            public static final int RED_STROBE = 0x0D;
            public static final int GREEN_STROBE = 0x0E;
            public static final int BLUE_STROBE = 0x0F;
            public static final int YELLOW_STROBE = 0x10;
            public static final int CYAN_STROBE = 0x11;
            public static final int PURPLE_STROBE = 0x12;
            public static final int WHITE_STROBE = 0x13;
            public static final int COLORFUL_JUMP = 0x14;
        }

        public static final class CustomMode {

            public static final int ASCEND_SHADE = 0x01;
            public static final int DESCEND_SHADE = 0x02;
            public static final int ASCEND_DESCEND_SHADE = 0x03;
            public static final int MIXED_SHADE = 0x04;
            public static final int JUMP = 0x05;
            public static final int STROBE = 0x06;
        }

        /**
         * red, green, blue range [0, 255]
         */
        public static class Color {

            private int red;
            private int green;
            private int blue;

            public int getRed() {
                return red;
            }

            public void setRed(int red) {
                this.red = red;
            }

            public int getGreen() {
                return green;
            }

            public void setGreen(int green) {
                this.green = green;
            }

            public int getBlue() {
                return blue;
            }

            public void setBlue(int blue) {
                this.blue = blue;
            }
        }
    }

    /**
     * Sunrise Sunset
     */

    public static class SunriseSunsetType {

        public static final byte SUNRISE = (byte) 0x1C;
        public static final byte SUNSET = (byte) 0x1D;
    }

    public static class SunriseSunsetActionType {

        public static final byte ON_OFF = (byte) 0x01;
        public static final byte SCENE = (byte) 0x02;
        public static final byte CUSTOM = (byte) 0x04;
    }

    public static abstract class SunriseSunsetAction {

        private byte type = SunriseSunsetType.SUNRISE;
        private boolean isEnabled = true;

        /**
         * @return MeshCommand.SunriseSunsetType
         */
        public byte getType() {
            return type;
        }

        /**
         * @param type MeshCommand.SunriseSunsetType
         */
        public void setType(byte type) {
            this.type = type;
        }

        /**
         * @return MeshCommand.SunriseSunsetActionType
         */
        public abstract byte getActionType();

        public boolean isEnabled() {
            return isEnabled;
        }

        public void setEnabled(boolean enabled) {
            isEnabled = enabled;
        }

        public abstract String getDescription();
    }

    public static class SunriseSunsetOnOffAction extends SunriseSunsetAction {

        private boolean isOn = true;
        private int duration = 0;

        /**
         * @param type MeshCommand.SunriseSunsetType
         */
        public SunriseSunsetOnOffAction(byte type) {
            super();
            this.setType(type);
        }

        @Override
        public byte getActionType() {
            return SunriseSunsetActionType.ON_OFF;
        }

        @Override
        public String getDescription() {
            return "OnOffAction " + getType() + ", isEnabled " + isEnabled() + ", " + isOn + ", duration " + duration;
        }

        public boolean isOn() {
            return isOn;
        }

        public void setOn(boolean on) {
            isOn = on;
        }

        public int getDuration() {
            return duration;
        }

        public void setDuration(int duration) {
            this.duration = duration;
        }
    }

    public static class SunriseSunsetSceneAction extends SunriseSunsetAction {

        /**
         * Range [1, 16], default 1
         */
        private int sceneID = 1;

        /**
         * @param type MeshCommand.SunriseSunsetType
         */
        public SunriseSunsetSceneAction(byte type) {
            super();
            this.setType(type);
        }

        @Override
        public byte getActionType() {
            return SunriseSunsetActionType.SCENE;
        }

        public int getSceneID() {
            return sceneID;
        }

        /**
         * @param sceneID Range [1, 16], default 1
         */
        public void setSceneID(int sceneID) {
            this.sceneID = sceneID;
        }

        @Override
        public String getDescription() {
            return "SceneAction " + getType() + ", isEnabled " + isEnabled() + ", sceneID " + sceneID;
        }
    }

    public static class SunriseSunsetCustomAction extends SunriseSunsetAction {

        /**
         * Range [0, 100], default 100
         */
        private int brightness = 100;
        /**
         * Range [0, 255], default 255
         */
        private int red = 255;
        /**
         * Range [0, 255], default 255
         */
        private int green = 255;
        /**
         * Range [0, 255], default 255
         */
        private int blue = 255;
        /**
         * CT range [0, 100], white range [0, 255], default 100
         */
        private int ctOrW = 255;
        /**
         * Range [0, 0xFFFF], default 0
         */
        private int duration = 0;

        /**
         * @param type MeshCommand.SunriseSunsetType
         */
        public SunriseSunsetCustomAction(byte type) {
            super();
            this.setType(type);
        }

        @Override
        public byte getActionType() {
            return SunriseSunsetActionType.CUSTOM;
        }

        public int getBrightness() {
            return brightness;
        }

        public void setBrightness(int brightness) {
            this.brightness = brightness;
        }

        public int getRed() {
            return red;
        }

        public void setRed(int red) {
            this.red = red;
        }

        public int getGreen() {
            return green;
        }

        public void setGreen(int green) {
            this.green = green;
        }

        public int getBlue() {
            return blue;
        }

        public void setBlue(int blue) {
            this.blue = blue;
        }

        public int getCtOrW() {
            return ctOrW;
        }

        public void setCtOrW(int ctOrW) {
            this.ctOrW = ctOrW;
        }

        public int getDuration() {
            return duration;
        }

        public void setDuration(int duration) {
            this.duration = duration;
        }

        @Override
        public String getDescription() {
            return "CustomAction " + getType() + ", isEnabled " + isEnabled() + ", Brightness"
                    + brightness + ", RGBW " + red + " " + green + " " + blue + " " + ctOrW
                    + ", duration " + duration;
        }
    }

    public static class Scene {

        private int sceneID;

        private int brightness = 100;

        private int red = 255;

        private int green = 255;

        private int blue = 255;

        private int ctOrW = 100;

        private int duration = 0;

        private boolean channel1On = false;

        private boolean channel2On = false;

        public Scene(int sceneID) {
            super();
            this.sceneID = sceneID;
        }

        public int getSceneID() {
            return sceneID;
        }

        /**
         * @param sceneID Range [1, 16]
         */
        public void setSceneID(int sceneID) {
            this.sceneID = sceneID;
        }

        public int getBrightness() {
            return brightness;
        }

        /**
         * @param brightness Range [0, 100]
         */
        public void setBrightness(int brightness) {
            this.brightness = brightness;
        }

        public int getRed() {
            return red;
        }

        /**
         * @param red Range [0, 255]
         */
        public void setRed(int red) {
            this.red = red;
        }

        public int getGreen() {
            return green;
        }

        /**
         * @param green Range [0, 255]
         */
        public void setGreen(int green) {
            this.green = green;
        }

        public int getBlue() {
            return blue;
        }

        /**
         * @param blue Range [0, 255]
         */
        public void setBlue(int blue) {
            this.blue = blue;
        }

        public int getCtOrW() {
            return ctOrW;
        }

        /**
         * @param ctOrW CCT range [0, 100], white range [0, 255]
         */
        public void setCtOrW(int ctOrW) {
            this.ctOrW = ctOrW;
        }

        public int getDuration() {
            return duration;
        }

        /**
         * @param duration Range [0, 65535]
         */
        public void setDuration(int duration) {
            this.duration = duration;
        }

        public boolean isChannel1On() {
            return channel1On;
        }

        public void setChannel1On(boolean channel1On) {
            this.channel1On = channel1On;
        }

        public boolean isChannel2On() {
            return channel2On;
        }

        public void setChannel2On(boolean channel2On) {
            this.channel2On = channel2On;
        }
    }

    public static class AlarmActionType {

        public static final int OFF = 0x00;
        public static final int ON = 0x01;
        public static final int SCENE = 0x02;
    }

    public static class AlarmDayType {

        public static final int DAY = 0x00;
        public static final int WEEK = 0x01;
    }

    public static abstract class AbstractAlarm {

        private int alarmID = 0;

        private int actionType = AlarmActionType.OFF;
        private boolean isEnabled = true;
        private int hour = 10;
        private int minute = 10;
        private int second = 0;
        private int sceneID;

        /**
         * @return MeshCommand.AlarmDayType.DAY or WEEK
         */
        public abstract int getDayType();

        public int getAlarmID() {
            return alarmID;
        }

        public void setAlarmID(int alarmID) {
            this.alarmID = alarmID;
        }

        public int getActionType() {
            return actionType;
        }

        public void setActionType(int actionType) {
            this.actionType = actionType;
        }

        public boolean isEnabled() {
            return isEnabled;
        }

        public void setEnabled(boolean enabled) {
            isEnabled = enabled;
        }

        public int getHour() {
            return hour;
        }

        public void setHour(int hour) {
            this.hour = hour;
        }

        public int getMinute() {
            return minute;
        }

        public void setMinute(int minute) {
            this.minute = minute;
        }

        public int getSecond() {
            return second;
        }

        public void setSecond(int second) {
            this.second = second;
        }

        public int getSceneID() {
            return sceneID;
        }

        public void setSceneID(int sceneID) {
            this.sceneID = sceneID;
        }

        public int getAlarmEvent() {

            return actionType | (getDayType() << 4) | (isEnabled ? 0x80 : 0x00);
        }
    }

    public static class DayAlarm extends AbstractAlarm {

        private int month = 1;
        private int day = 1;

        public DayAlarm(int alarmID) {
            super();
            this.setAlarmID(alarmID);
        }

        @Override
        public int getDayType() {
            return AlarmDayType.DAY;
        }

        public int getMonth() {
            return month;
        }

        public void setMonth(int month) {
            this.month = month;
        }

        public int getDay() {
            return day;
        }

        public void setDay(int day) {
            this.day = day;
        }
    }

    public static class WeekAlarm extends AbstractAlarm {

        private int week = 0;

        public WeekAlarm(int alarmID) {
            super();
            this.setAlarmID(alarmID);
        }

        @Override
        public int getDayType() {
            return AlarmDayType.WEEK;
        }

        /**
         * @return bit0 Sun, bit1 Mon, bit2 Tue, bit3 Wed, bit4 Thu, bit5 Fri, bit6 Sat, bit7 must be 0.
         */
        public int getWeek() {
            return week;
        }

        /**
         * @param week bit0 Sun, bit1 Mon, bit2 Tue, bit3 Wed, bit4 Thu, bit5 Fri, bit6 Sat, bit7 must be 0.
         */
        public void setWeek(int week) {
            this.week = week;
        }
    }

    public static class SensorReportType {

        /// Reserved type.
        static final int RESERVED = 0x00;

        /// Door Sensor State, false -> Closed, true -> Open.
        static final int DOOR_STATE = 0x01;

        /// PIR Motion State, false -> Not Detected, true -> Detected.
        static final int PIR_MOTION = 0x02;

        /// Microwave Motion State, false -> Not Detected, true -> Detected.
        static final int MICROWAVE_MOTION = 0x03;

        /// LUX, int value.
        static final int LUX = 0x04;

        /// Temperature, int value
        static final int TEMPERATURE = 0x05;
    }

    public static class SensorReportKey {

        /// false -> Closed, true -> Open.
        static final String DOOR_STATE = "DOOR_STATE";

        /// false -> Not Detected, true -> Detected.
        static final String DETECTED = "DETECTED";

        /// LUX, int value.
        static final String LUX = "LUX";

        /// Temperature, int value.
        static final String TEMPERATURE = "TEMPERATURE";
    }

    public static class SensorAttributeType {

        /// Value range is [0, 15], default is 1.
        public static final int HUMAN_INDUCTION_SENSITIVITY = 0x21;

        /// 0x00 Off, 0x01 On, default is On.
        public static final int MICROWAVE_MODULE_ON_OFF_STATE = 0x22;

        /// 0x00 Off, 0x01 On, default is On.
        public static final int LIGHT_MODULE_ON_OFF_STATE = 0x23;

        /// Value range is [0, 1000], default is 0 LUX.
        public static final int WORKING_BRIGHTNESS_THRESHOLD = 0x24;

        /// Value range is [0, 65535], default is 60 seconds.
        public static final int DETECTED_PWM_OUTPUT_DELAY = 0x25;

        /// Value range is [0, 1000], default is 0 LUX.
        public static final int DETECTED_PWM_OUTPUT_BRIGHTNESS = 0x26;

        /// Value range is [0, 100], default is 100%.
        public static final int DETECTED_PWM_OUTPUT_PERCENTAGE = 0x27;

        /// Value range is [0, 65535], default is 60s.
        public static final int NOT_DETECTED_PWM_OUTPUT_DELAY = 0x28;

        /// Value range is [0, 100], default is 10%.
        public static final int NOT_DETECTED_PWM_OUTPUT_PERCENTAGE = 0x29;

        /// Value range is [0, 100], default is 0%.
        public static final int PWM_OUTPUT_PERCENTAGE_AFTER_NOT_DETECTED_DELAY = 0x2A;

        /// 0x00 Auto mode, 0x01 Independent, default is Auto.
        public static final int WORKING_MODE = 0x30;

        /// Readonly
        public static final int SENSOR_STATE = 0x31;

        /// Value range is [100, 65535], default is 1000ms.
        public static final int STATE_REPORT_INTERVAL = 0x32;

        /// 0 Off, 1 On, default is On.
        public static final int REPORT_ON_OFF_STATE = 0x33;

        /// Value range is [-127, 127], default is 0.
        public static final int LUX_ZERO_DEVIATION_OF_THE_BRIGHTNESS_SENSOR = 0x34;

        /// Value range is [1, 100], K = (value / 10.0), default is 10 (K = 1.0).
        public static final int LUX_SCALE_FACTOR_OF_THE_BRIGHTNESS_SENSOR = 0x35;
    }

    public static class SensorEvent {

        public static final int DOOR_OPEN = 0x41;
        public static final int DOOR_CLOSED = 0x51;

        public static final int PIR_DETECTED = 0x42;
        public static final int PIR_NOT_DETECTED = 0x52;

        public static final int MICROWAVE_DETECTED = 0x43;
        public static final int MICROWAVE_NOT_DETECTED = 0x53;

        /*
        case luxSensor = 0x44
        case temperatureSesor = 0x45
        */

    }

    /**
     * 用例
     * <p>
     * SmartSwitchActions actions = new SmartSwitchActions();
     * // 获取智能开关支持的键数
     * List<Byte> allButtonCount = SmartSwitchActions.ButtonCount.all;
     * // 当前选中的键数
     * actions.getButtonCount();
     * // 更新智能开关的键数
     * actions.setButtonCount(SmartSwitchActions.ButtonCount.key2);
     * // 获取更新键数后的所有键位置
     * List<String> currentPositions = SmartSwitchActions.ButtonCount.getPositions(SmartSwitchActions.ButtonCount.key2);
     * // 获取当前选中的键位置
     * actions.getButtonPosition();
     * // 更新键位置
     * actions.setButtonPosition(SmartSwitchActions.ButtonPosition.key2Left);
     * // 短按动作
     * actions.getShortPress();
     * actions.setShortPress(SmartSwitchActions.ShortPress.turnOn);
     * actions.setShortPress(SmartSwitchActions.ShortPress.setRed);
     * actions.setRed(100);
     * // 长按动作
     * actions.setLongPress(SmartSwitchActions.LongPress.moveBrightnessUp);
     * // 所有动作数组
     * List<Byte> allShortPress = SmartSwitchActions.ShortPress.all;
     * List<Byte> allLongPress = SmartSwitchActions.LongPress.all;
     * <p>
     * int address = 0x01;
     * String switchIdText = "7000000D";
     * // 获取开关 ID 并判断是否有效
     * long switchId = MeshCommand.getSmartSwitchId(switchIdText);
     * if (switchId == 0) {
     * // 无效的 switchId
     * } else {
     * // 保存智能开关
     * List<MeshCommand> commands = MeshCommand.saveSmartSwitch(address, switchId, actions);
     * // 每条命令至少间隔 1 秒
     * MeshManager.getInstance().sendCommands(commands, 1000);
     * // 删除智能开关
     * MeshCommand deleteCmd = MeshCommand.deleteSavedSmartSwitch(address, switchId, actions.getButtonPosition());
     * MeshManager.getInstance().send(deleteCmd);
     * }
     */
    public static class SmartSwitchActions {

        public static class ShortPress {
            public static final byte turnOn = 0x01;
            public static final byte turnOff = 0x02;
            public static final byte turnOnOff = 0x03;
            public static final byte turnOnWhite = 0x04;
            public static final byte turnOffWhite = 0x05;
            public static final byte turnOnOffWhite = 0x06;
            public static final byte stepBrightnessUp = 0x07;
            public static final byte stepBrightnessDown = 0x08;
            public static final byte stepBrightnessUpDown = 0x09;
            public static final byte recallScene1 = 0x29;
            public static final byte recallScene2 = 0x2B;
            public static final byte recallScene3 = 0x2D;
            public static final byte recallScene4 = 0x2F;
            public static final byte changeCct = 0x30;
            public static final byte changeRgb = 0x32;
            public static final byte runBuiltinRunningMode = 0x34;
            public static final byte setBrightness = 0x35;
            public static final byte setCct = 0x36;
            public static final byte setRed = 0x37;
            public static final byte setGreen = 0x38;
            public static final byte setBlue = 0x39;
            public static final byte setRgb = 0x3A;

            public static final List<Byte> all = new ArrayList<Byte>() {
                {
                    add(turnOn);
                    add(turnOff);
                    add(turnOnOff);
                    add(turnOnWhite);
                    add(turnOffWhite);
                    add(turnOnOffWhite);
                    add(stepBrightnessUp);
                    add(stepBrightnessDown);
                    add(stepBrightnessUpDown);
                    add(recallScene1);
                    add(recallScene2);
                    add(recallScene3);
                    add(recallScene4);
                    add(changeCct);
                    add(changeRgb);
                    add(runBuiltinRunningMode);
                    add(setBrightness);
                    add(setCct);
                    add(setRed);
                    add(setGreen);
                    add(setBlue);
                    add(setRgb);
                }
            };
        }

        public static class LongPress {
            public static final byte undefined = 0x00;
            public static final byte moveBrightnessUp = 0x11;
            public static final byte moveBrightnessDown = 0x12;
            public static final byte moveBrightnessUpDown = 0x13;
            public static final byte moveCctWhiteUp = 0x15;
            public static final byte moveCctWhiteDown = 0x16;
            public static final byte moveCctWhiteUpDown = 0x17;
            public static final byte moveRgbMixFadeUp = 0x1D;
            public static final byte moveRgbMixFadeDown = 0x1E;
            public static final byte moveRgbMixFadeUpDown = 0x1F;
            public static final byte moveSpeedUp = 0x25;
            public static final byte moveSpeedDown = 0x26;
            public static final byte moveSpeedUpDown = 0x27;
            public static final byte saveScene1 = 0x28;
            public static final byte recallScene1 = 0x29;
            public static final byte saveScene2 = 0x2A;
            public static final byte recallScene2 = 0x2B;
            public static final byte saveScene3 = 0x2C;
            public static final byte recallScene3 = 0x2D;
            public static final byte saveScene4 = 0x2E;
            public static final byte recallScene4 = 0x2F;

            public static final List<Byte> all = new ArrayList<Byte>() {
                {
                    add(undefined);
                    add(moveBrightnessUp);
                    add(moveBrightnessDown);
                    add(moveBrightnessUpDown);
                    add(moveCctWhiteUp);
                    add(moveCctWhiteDown);
                    add(moveCctWhiteUpDown);
                    add(moveRgbMixFadeUp);
                    add(moveRgbMixFadeDown);
                    add(moveRgbMixFadeUpDown);
                    add(moveSpeedUp);
                    add(moveSpeedDown);
                    add(moveSpeedUpDown);
                    add(saveScene1);
                    add(recallScene1);
                    add(saveScene2);
                    add(recallScene2);
                    add(saveScene3);
                    add(recallScene3);
                    add(saveScene4);
                    add(recallScene4);
                }
            };
        }

        public static class LongPressStop {
            public static final byte moveBrightnessStop = 0x10;
            public static final byte moveCctStop = 0x14;
            public static final byte moveRgbMixFadeStop = 0x1C;
            public static final byte moveSpeedStop = 0x24;
        }

        public static class ButtonCount {
            public static final byte key1 = 9;
            public static final byte key2 = 8;
            public static final byte key4 = 7;

            public static final List<Byte> all = new ArrayList<Byte>() {
                {
                    add(key1);
                    add(key2);
                    add(key4);
                }
            };

            public static byte getSwitchModel(byte countKey) {
                switch (countKey) {
                    case key1:
                        return 9;
                    case key2:
                        return 8;
                    default:
                        return 7;
                }
            }

            /**
             * @param countKey ButtonCount.key?
             * @return The List<ButtonPosition.key?> at the ButtonCount.Key?
             */
            public static List<String> getPositions(byte countKey) {
                switch (countKey) {
                    case key1:
                        return new ArrayList<String>() {
                            {
                                add(ButtonPosition.key1);
                            }
                        };
                    case key2:
                        return new ArrayList<String>() {
                            {
                                add(ButtonPosition.key2Left);
                                add(ButtonPosition.key2Right);
                            }
                        };
                    default:
                        return new ArrayList<String>() {
                            {
                                add(ButtonPosition.key4TopLeft);
                                add(ButtonPosition.key4TopRight);
                                add(ButtonPosition.key4BottomLeft);
                                add(ButtonPosition.key4BottomRight);
                            }
                        };
                }
            }
        }

        public static class ButtonPosition {
            public static final String key4TopLeft = "key4TopLeft";
            public static final String key4BottomLeft = "key4BottomLeft";
            public static final String key4TopRight = "key4TopRight";
            public static final String key4BottomRight = "key4BottomRight";
            public static final String key2Left = "key2Left";
            public static final String key2Right = "key2Right";
            public static final String key1 = "key1";

            public static byte getKeyIndex(String position) {
                switch (position) {
                    case key4BottomRight:
                    case key2Left:
                        return 2;
                    case key4TopLeft:
                        return 3;
                    case key4BottomLeft:
                        return 4;
                    default:
                        return 1;
                }
            }

            public static byte getGroupIndex(String position) {
                return (byte) (1 << (getKeyIndex(position) - 1));
            }
        }

        public static class ButtonEvent {

            public static final byte shortPress = 0x01;
            public static final byte longPressBegin = 0x02;
            public static final byte longPressEnd = 0x03;
        }

        private byte buttonCount = ButtonCount.key4;
        private String buttonPosition = ButtonPosition.key4TopLeft;
        private byte shortPress = ShortPress.turnOnOff;
        private byte longPress = LongPress.undefined;

        private int brightness = 100;
        private int cct = 50;
        private int red = 255;
        private int green = 255;
        private int blue = 255;

        public byte getButtonCount() {
            return buttonCount;
        }

        public void setButtonCount(byte buttonCount) {
            this.buttonCount = buttonCount;
        }

        public String getButtonPosition() {
            return buttonPosition;
        }

        public void setButtonPosition(String buttonPosition) {
            this.buttonPosition = buttonPosition;
        }

        public byte getShortPress() {
            return shortPress;
        }

        public void setShortPress(byte shortPress) {
            this.shortPress = shortPress;
        }

        public byte getLongPress() {
            return longPress;
        }

        public void setLongPress(byte longPress) {
            this.longPress = longPress;
        }

        public int getBrightness() {
            return brightness;
        }

        public void setBrightness(int brightness) {
            this.brightness = brightness;
        }

        public int getCct() {
            return cct;
        }

        public void setCct(int cct) {
            this.cct = cct;
        }

        public int getRed() {
            return red;
        }

        public void setRed(int red) {
            this.red = red;
        }

        public int getGreen() {
            return green;
        }

        public void setGreen(int green) {
            this.green = green;
        }

        public int getBlue() {
            return blue;
        }

        public void setBlue(int blue) {
            this.blue = blue;
        }

        public static SmartSwitchActions defaultActions = new SmartSwitchActions();
    }

    /**
     * @param text
     * @return If text == null or text.length() != 8, return 0, else return a valid switch ID.
     */
    public static long getSmartSwitchId(String text) {
        if (text == null) {
            return 0;
        }
        if (text.length() == 8) {
            byte[] bytes = HexUtil.getBytesByString(text);
            return (0xff & bytes[3]) | (0xff00 & (bytes[2] << 8)) | (0xff0000 & (bytes[1] << 16)) | (0xff000000 & (bytes[0] << 24));
        } else if (text.length() > 8) {
            String[] items = text.split("\\+");
            if (items.length > 0) {
                String item = items[0];
                int itemLen = item.length();
                String itemText = item.substring(itemLen - 8, itemLen);
                byte[] bytes = HexUtil.getBytesByString(itemText);
                return (0xff & bytes[3]) | (0xff00 & (bytes[2] << 8)) | (0xff0000 & (bytes[1] << 16)) | (0xff000000 & (bytes[0] << 24));
            }
        }
        return 0;
    }

    public static List<MeshCommand> saveSmartSwitch(int address, long switchId, SmartSwitchActions actions) {
        ArrayList<MeshCommand> commands = new ArrayList<>();

        MeshCommand bind = new MeshCommand();
        bind.tag = Const.TAG_APP_TO_NODE;
        bind.dst = address;
        bind.userData[0] = 0x12;
        bind.userData[1] = 0x03; // smart switch
        bind.userData[2] = 0x01; // bind
        bind.userData[3] = SmartSwitchActions.ButtonPosition.getGroupIndex(actions.buttonPosition);
        bind.userData[4] = (byte) ((switchId >> 24) & 0xFF);
        bind.userData[5] = (byte) ((switchId >> 16) & 0xFF);
        bind.userData[6] = (byte) ((switchId >> 8) & 0xFF);
        bind.userData[7] = (byte) (switchId & 0xFF);
        commands.add(bind);

        // Press Type
        // Short press 1
        // Long press start 2
        // Long press end 3

        // Short press action
        MeshCommand shortPress = new MeshCommand();
        shortPress.tag = Const.TAG_APP_TO_NODE;
        shortPress.dst = address;
        shortPress.userData[0] = 0x13;
        shortPress.userData[1] = 0x00; // default group
        shortPress.userData[2] = SmartSwitchActions.ButtonPosition.getKeyIndex(actions.buttonPosition);
        shortPress.userData[3] = 1; // press type, short press
        shortPress.userData[4] = actions.shortPress; // action index
        shortPress.userData[5] = 0x07; // Switch Model is always Mode 7
        switch (actions.shortPress) {
            case SmartSwitchActions.ShortPress.setBrightness:
                shortPress.userData[6] = (byte) actions.brightness;
                break;
            case SmartSwitchActions.ShortPress.setCct:
                shortPress.userData[6] = (byte) actions.cct;
                break;
            case SmartSwitchActions.ShortPress.setRed:
                shortPress.userData[6] = (byte) actions.red;
                break;
            case SmartSwitchActions.ShortPress.setGreen:
                shortPress.userData[6] = (byte) actions.green;
                break;
            case SmartSwitchActions.ShortPress.setBlue:
                shortPress.userData[6] = (byte) actions.blue;
                break;
            case SmartSwitchActions.ShortPress.setRgb:
                shortPress.userData[6] = (byte) actions.red;
                shortPress.userData[7] = (byte) actions.green;
                shortPress.userData[8] = (byte) actions.blue;
                break;
            default:
                break;
        }
        commands.add(shortPress);

        // Long press start action
        MeshCommand longPress = new MeshCommand();
        longPress.tag = Const.TAG_APP_TO_NODE;
        longPress.dst = address;
        longPress.userData[0] = 0x13;
        longPress.userData[1] = 0x00; // default group
        longPress.userData[2] = SmartSwitchActions.ButtonPosition.getKeyIndex(actions.buttonPosition);
        longPress.userData[3] = 2; // press type, long press
        longPress.userData[4] = actions.longPress; // action index
        longPress.userData[5] = 0x07; // Switch Model is always Mode 7
        commands.add(longPress);

        // Long press stop action
        MeshCommand temp = new MeshCommand();
        temp.tag = Const.TAG_APP_TO_NODE;
        temp.dst = address;
        temp.userData[0] = 0x13;
        temp.userData[1] = 0x00; // default group
        temp.userData[2] = SmartSwitchActions.ButtonPosition.getKeyIndex(actions.buttonPosition);
        temp.userData[3] = 3; // press type, long press stop
        temp.userData[4] = SmartSwitchActions.LongPressStop.moveBrightnessStop; // action index
        temp.userData[5] = 0x07; // Switch Model is always Mode 7

        MeshCommand stop = null;
        switch (actions.longPress) {
            case SmartSwitchActions.LongPress.moveBrightnessUp:
            case SmartSwitchActions.LongPress.moveBrightnessDown:
            case SmartSwitchActions.LongPress.moveBrightnessUpDown:
                // temp.userData[4] = SmartSwitchActions.LongPressStop.moveBrightnessStop; // action index
                stop = temp;
                break;

            case SmartSwitchActions.LongPress.moveCctWhiteUp:
            case SmartSwitchActions.LongPress.moveCctWhiteDown:
            case SmartSwitchActions.LongPress.moveCctWhiteUpDown:
                temp.userData[4] = SmartSwitchActions.LongPressStop.moveCctStop; // action index
                stop = temp;
                break;

            case SmartSwitchActions.LongPress.moveRgbMixFadeUp:
            case SmartSwitchActions.LongPress.moveRgbMixFadeDown:
            case SmartSwitchActions.LongPress.moveRgbMixFadeUpDown:
                temp.userData[4] = SmartSwitchActions.LongPressStop.moveRgbMixFadeStop; // action index
                stop = temp;
                break;

            case SmartSwitchActions.LongPress.moveSpeedUp:
            case SmartSwitchActions.LongPress.moveSpeedDown:
            case SmartSwitchActions.LongPress.moveSpeedUpDown:
                temp.userData[4] = SmartSwitchActions.LongPressStop.moveSpeedStop; // action index
                stop = temp;
                break;

            default:
                break;
        }

        if (stop != null) {
            commands.add(stop);
        }
        return commands;
    }

    /**
     * Delete the smart switch save by `saveSmartSwitch`.
     */
    public static MeshCommand deleteSavedSmartSwitch(int address, long switchId, String buttonPosition) {
        MeshCommand unbind = new MeshCommand();
        unbind.tag = Const.TAG_APP_TO_NODE;
        unbind.dst = address;
        unbind.userData[0] = 0x12;
        unbind.userData[1] = 0x03; // smart switch
        unbind.userData[2] = 0x02; // unbind
        unbind.userData[3] = SmartSwitchActions.ButtonPosition.getGroupIndex(buttonPosition);
        unbind.userData[4] = (byte) ((switchId >> 24) & 0xFF);
        unbind.userData[5] = (byte) ((switchId >> 16) & 0xFF);
        unbind.userData[6] = (byte) ((switchId >> 8) & 0xFF);
        unbind.userData[7] = (byte) (switchId & 0xFF);
        return unbind;
    }

    /**
     * You have to recall `disableDynamicControl` before `setDynamicControl`.
     *
     * @param address
     * @param dynamicControl
     * @return
     */
    public static ArrayList<MeshCommand> setDynamicControl(int address, DynamicControl dynamicControl) {

        ArrayList<MeshCommand> commands = new ArrayList<>();
        if (!dynamicControl.isValid()) {
            return commands;
        }

        int startIndex = dynamicControl.isTime() ? (0x60 - 1) : (0x68 - 1);

        if (dynamicControl.isTime()) {
            for (DynamicControl.Item item : dynamicControl.getItems()) {
                MeshCommand cmd = new MeshCommand();
                cmd.tag = Const.TAG_APP_TO_NODE;
                cmd.dst = address;
                cmd.userData[0] = (byte) ((startIndex + dynamicControl.getIndex()) & 0xFF);
                cmd.userData[1] = (byte) (item.getHour() & 0xFF);
                cmd.userData[2] = (byte) (item.getMinute() & 0xFF);
                cmd.userData[3] = (byte) (item.getBrightness() & 0xFF);
                cmd.userData[4] = (byte) (item.getRed() & 0xFF);
                cmd.userData[5] = (byte) (item.getGreen() & 0xFF);
                cmd.userData[6] = (byte) (item.getBlue() & 0xFF);
                cmd.userData[7] = (byte) (item.getWhiteOrCt() & 0xFF);
                cmd.userData[8] = (byte) (item.isWhiteOrCtEnabled() ? 0x00 : 0x01);
                commands.add(cmd);
            }
        } else {
            for (DynamicControl.Item item : dynamicControl.getItems()) {
                MeshCommand cmd = new MeshCommand();
                cmd.tag = Const.TAG_APP_TO_NODE;
                cmd.dst = address;
                cmd.userData[0] = (byte) ((startIndex + dynamicControl.getIndex()) & 0xFF);
                cmd.userData[1] = (byte) ((item.getDelay() >> 8) & 0xFF);
                cmd.userData[2] = (byte) (item.getDelay() & 0xFF);
                cmd.userData[3] = (byte) (item.getBrightness() & 0xFF);
                cmd.userData[4] = (byte) (item.getRed() & 0xFF);
                cmd.userData[5] = (byte) (item.getGreen() & 0xFF);
                cmd.userData[6] = (byte) (item.getBlue() & 0xFF);
                cmd.userData[7] = (byte) (item.getWhiteOrCt() & 0xFF);
                cmd.userData[8] = (byte) (item.isWhiteOrCtEnabled() ? 0x00 : 0x01);
                commands.add(cmd);
            }
        }
        commands.add(stopSetDynamicControl(address));
        return commands;
    }

    public static MeshCommand stopSetDynamicControl(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x60;
        cmd.userData[1] = (byte) 0x80; // stop
        return cmd;
    }

    /**
     * dynamic control index range [1, 16], item index range [1, 32]
     */
    public static MeshCommand getDynamicControl(int address, int dynamicControlIndex, int itemIndex) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) ((0x60 - 1 + dynamicControlIndex) & 0xFF);
        cmd.userData[1] = (byte) 0xD8; // get
        cmd.userData[2] = (byte) ((itemIndex - 1) & 0xFF);
        return cmd;
    }

    /**
     * Start (enable) run dynamic control, dynamic control index range [1, 16], if repeat count is 0x00 that means always repeat.
     */
    public static MeshCommand enableDynamicControl(int address, int dynamicControlIndex, int repeatCount) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) ((0x60 - 1 + dynamicControlIndex) & 0xFF);
        cmd.userData[1] = (byte) 0xA0; // start run
        cmd.userData[2] = (byte) (repeatCount & 0xFF);
        return cmd;
    }

    /**
     * Stop (disable) stop run dynamic control
     */
    public static MeshCommand disableDynamicControl(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x60;
        cmd.userData[1] = (byte) 0xB0; // stop run
        return cmd;
    }

    public static MeshCommand resetDynamicControl(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x60;
        cmd.userData[1] = (byte) 0xC0; // clear
        return cmd;
    }

    public static MeshCommand getDynamicControlCurrentState(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x60;
        cmd.userData[1] = (byte) 0xD0; // current state
        return cmd;
    }

    public static MeshCommand getDynamicControlTargetState(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x60;
        cmd.userData[1] = (byte) 0xD1; // target state
        return cmd;
    }

    /**
     * You have to recall `disableNaturalLight` before `setNaturalLight`.
     *
     * @param address
     * @param naturalLight
     * @param mode
     * @return
     */
    static ArrayList<MeshCommand> setNaturalLight(int address, NaturalLight naturalLight, NaturalLight.Mode mode) {
        DynamicControl dynamicControl = new DynamicControl();
        dynamicControl.setIndex(NaturalLight.getModeValue(mode));
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            naturalLight.getItems().forEach((item) -> {
                DynamicControl.Item dcItem = new DynamicControl.Item();
                dcItem.setHour(item.getHour());
                dcItem.setBrightness(item.getBrightness());
                dcItem.setWhiteOrCt(item.getCct());
                dcItem.setRed(0);
                dcItem.setGreen(0);
                dcItem.setBlue(0);
                dcItem.setWhiteOrCtEnabled(true);
                dynamicControl.getItems().add(dcItem);
            });
        }
        return setDynamicControl(address, dynamicControl);
    }

    /**
     * item index range [1, 24]
     */
    static MeshCommand getNaturalLight(int address, NaturalLight.Mode mode, int hour) {
        return getDynamicControl(address, NaturalLight.getModeValue(mode), hour + 1);
    }

    public static MeshCommand enableNaturalLight(int address, NaturalLight.Mode mode) {
        return enableDynamicControl(address, NaturalLight.getModeValue(mode), 0x00);
    }

    public static MeshCommand disableNaturalLight(int address) {
        return disableDynamicControl(address);
    }

    public static MeshCommand resetNaturalLight(int address) {
        return resetDynamicControl(address);
    }

    static MeshCommand getNaturalLightCurrentState(int address) {
        return getDynamicControlCurrentState(address);
    }

    public static MeshCommand getStatus(int address) {
        return getStatusV2(address);
//        MeshCommand cmd = new MeshCommand();
//        cmd.tag = Const.TAG_GET_STATUS;
//        cmd.dst = address;
//        return cmd;
    }

    public static MeshCommand getStatusV2(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x78;
        cmd.userData[1] = (byte) 0x00;
        return cmd;
    }

    public static class UartDali {

        public static MeshCommand sendUartData(int address, byte[] data) {
            byte[] uartData = data;
            if (data.length != 10) {
                uartData = new byte[]{
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                };
            }
            MeshCommand cmd = new MeshCommand();
            cmd.tag = Const.TAG_UART_MODULE;
            cmd.dst = address;
            cmd.param = (int) uartData[0];
            for (int i = 0; i < cmd.userData.length; i++) {
                cmd.userData[i] = uartData[i + 1];
            }
            return cmd;
        }

        /**
         * address: DALI Gateway ble short address.
         * daliAddr: Device address (0x00-0x3F), group address (0x80-0x8F), or broadcast 0xFF (all devices).
         */

        public static final int COMMAND_TYPE_DISCOVER = 0xA0;
        public static final int COMMAND_TYPE_TERMINATE = 0xAF;
        public static final int COMMAND_TYPE_CONFIG = 0x01;
        public static final int COMMAND_TYPE_CONTROL = 0x02;
        public static final int COMMAND_TYPE_QUERY = 0x03;

        public static final int COMMAND_TYPE_SET_POWER_ON = 0x30;
        public static final int COMMAND_TYPE_SET_SYSTEM_FAIL = 0x31;
        public static final int COMMAND_TYPE_SET_SCENE_0 = 0x40;
        public static final int COMMAND_TYPE_SET_SCENE_1 = 0x41;
        public static final int COMMAND_TYPE_SET_SCENE_2 = 0x42;
        public static final int COMMAND_TYPE_SET_SCENE_3 = 0x43;
        public static final int COMMAND_TYPE_SET_SCENE_4 = 0x44;
        public static final int COMMAND_TYPE_SET_SCENE_5 = 0x45;
        public static final int COMMAND_TYPE_SET_SCENE_6 = 0x46;
        public static final int COMMAND_TYPE_SET_SCENE_7 = 0x47;
        public static final int COMMAND_TYPE_SET_SCENE_8 = 0x48;
        public static final int COMMAND_TYPE_SET_SCENE_9 = 0x49;
        public static final int COMMAND_TYPE_SET_SCENE_10 = 0x4A;
        public static final int COMMAND_TYPE_SET_SCENE_11 = 0x4B;
        public static final int COMMAND_TYPE_SET_SCENE_12 = 0x4C;
        public static final int COMMAND_TYPE_SET_SCENE_13 = 0x4D;
        public static final int COMMAND_TYPE_SET_SCENE_14 = 0x4E;
        public static final int COMMAND_TYPE_SET_SCENE_15 = 0x4F;

        public static final int COMMAND_TYPE_COMMAND_OK = 0x80;
        public static final int COMMAND_TYPE_COMMAND_ERROR = 0x81;

        public static final int CONFIG_RESET = 0x00;
        public static final int CONFIG_SET_MAX = 0x01;
        public static final int CONFIG_SET_MIN = 0x02;
        public static final int CONFIG_SET_SYSTEM_FAIL_DT6 = 0x03;
        public static final int CONFIG_SET_POWER_ON_DT6 = 0x04;
        public static final int CONFIG_SET_FADE_TIME = 0x05;
        public static final int CONFIG_SET_FADE_RATE = 0x06;
        public static final int CONFIG_SET_EXTENDED_FADE_TIME = 0x07;
        public static final int CONFIG_ADD_TO_GROUP = 0x08;
        public static final int CONFIG_REMOVE_FROM_GROUP = 0x09;
        public static final int CONFIG_SET_DIMMING_CURVE = 0x0A;
        public static final int CONFIG_SET_FAST_FADE_TIME = 0x0B;
        public static final int CONFIG_SET_OPERATE_MODE = 0x0C;
        public static final int CONFIG_STORE_DTR_AS_SHORT_ADDRESS = 0x0D;
        public static final int CONFIG_SET_SCENE_DT6 = 0x0E;
        public static final int CONFIG_REMOVE_FROM_SCENE = 0x0F;

        public static final int CONFIG_SET_CCT_STEP = 0x20;
        public static final int CONFIG_SET_CCT_COOLEST = 0x21;
        public static final int CONFIG_SET_CCT_WARMEST = 0x22;
        public static final int CONFIG_SET_CCT_PHYSICAL_COOLEST = 0x23;
        public static final int CONFIG_SET_CCT_PHYSICAL_WARMEST = 0x24;
        public static final int CONFIG_SET_RGBWAF_CONTROL = 0x25;

        public static final int CONFIG_SET_POWER_ON_DT8_XY = 0x30;
        public static final int CONFIG_SET_POWER_ON_DT8_CCT = 0x31;
        public static final int CONFIG_SET_SYSTEM_FAIL_DT8_XY = 0x32;
        public static final int CONFIG_SET_SYSTEM_FAIL_DT8_CCT = 0x33;
        public static final int CONFIG_SET_SCENE_DT8_XY = 0x34;
        public static final int CONFIG_SET_SCENE_DT8_CCT = 0x35;

        public static MeshCommand configDevice(int address, int daliAddr, int config) {
            return configDevice(address, daliAddr, config, new byte[0]);
        }

        /**
         * @param address  ble device short address
         * @param daliAddr dali device address
         * @param config   Use MeshCommand.UartDali.CONFIG_XXXX
         * @param values
         * @return
         */
        public static MeshCommand configDevice(int address, int daliAddr, int config, byte[] values) {
            byte[] data = new byte[]{
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
            };
            data[0] = (byte) 0xA5;
            data[1] = (byte) COMMAND_TYPE_CONFIG;
            data[2] = (byte) daliAddr;
            data[3] = (byte) config;
            if (values.length > 0 && values.length <= 6) {
                for (int i = 0; i < values.length; i++) {
                    data[i + 4] = values[i];
                }
            }
            return sendUartData(address, data);
        }

        public static MeshCommand setPowerOnDt8Rgb(int address, int daliAddr, byte[] values) {
            byte[] data = new byte[]{
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
            };
            data[0] = (byte) 0xA5;
            data[1] = (byte) COMMAND_TYPE_SET_POWER_ON;
            data[2] = (byte) daliAddr;
            if (values.length == 7) {
                for (int i = 3; i <= 9; i++) {
                    data[i] = values[i - 3];
                }
            }
            return sendUartData(address, data);
        }

        public static MeshCommand setSystemFailDt8Rgb(int address, int daliAddr, byte[] values) {
            byte[] data = new byte[]{
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
            };
            data[0] = (byte) 0xA5;
            data[1] = COMMAND_TYPE_SET_SYSTEM_FAIL;
            data[2] = (byte) daliAddr;
            if (values.length == 7) {
                for (int i = 3; i <= 9; i++) {
                    data[i] = values[i - 3];
                }
            }
            return sendUartData(address, data);
        }

        public static MeshCommand setSceneDt8Rgb(int address, int daliAddr, int sceneId, byte[] values) {
            byte[] data = new byte[]{
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
            };
            data[0] = (byte) 0xA5;
            data[1] = (byte) (COMMAND_TYPE_SET_SCENE_0 + sceneId);
            data[2] = (byte) daliAddr;
            if (values.length == 7) {
                for (int i = 3; i <= 9; i++) {
                    data[i] = values[i - 3];
                }
            }
            return sendUartData(address, data);
        }

        public static int CONTROL_DIRECT_ARC_POWER_CONTROL = 0x00;
        public static int CONTROL_OFF = 0x01;
        public static int CONTROL_UP = 0x02;
        public static int CONTROL_DOWN = 0x03;
        public static int CONTROL_STEP_UP = 0x04;
        public static int CONTROL_STEP_DOWN = 0x05;
        public static int CONTROL_STEP_DOWN_AND_OFF = 0x06;
        public static int CONTROL_ON_AND_STEP_UP = 0x07;
        public static int CONTROL_RECALL_MAX_LEVEL = 0x08;
        public static int CONTROL_RECALL_MIN_LEVEL = 0x09;
        public static int CONTROL_GO_TO_SCENE = 0x0A;
        public static int CONTROL_GO_TO_LAST_LEVEL = 0x0B;
        public static int CONTROL_X_COORDINATE_STEP_UP = 0x10;
        public static int CONTROL_X_COORDINATE_STEP_DOWN = 0x11;
        public static int CONTROL_Y_COORDINATE_STEP_UP = 0x12;
        public static int CONTROL_Y_COORDINATE_STEP_DOWN = 0x13;
        public static int CONTROL_CCT_STEP_COOLER = 0x14;
        public static int CONTROL_CCT_STEP_WARMER = 0x15;

        public static int CONTROL_ACTIVATE_XY = 0x20;
        public static int CONTROL_ACTIVATE_CCT = 0x21;
        public static int CONTROL_ACTIVATE_RGBWAF = 0x22;

        public static MeshCommand controlDevice(int address, int daliAddr, int control) {
            return controlDevice(address, daliAddr, control, new byte[0]);
        }

        /**
         * @param address
         * @param daliAddr
         * @param control  MeshCommand.UartDali.CONTROL_XXXX
         * @param values
         * @return
         */
        public static MeshCommand controlDevice(int address, int daliAddr, int control, byte[] values) {
            byte[] data = new byte[]{
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
            };
            data[0] = (byte) 0xA5;
            data[1] = (byte) COMMAND_TYPE_CONTROL;
            data[2] = (byte) daliAddr;
            data[3] = (byte) control;
            if (values.length > 0 && values.length <= 6) {
                for (int i = 0; i < values.length; i++) {
                    data[i + 4] = values[i];
                }
            }
            return sendUartData(address, data);
        }

        public static int QUERY_STATUS = 0x00;
        public static int QUERY_CONTROL_GEAR = 0x01;
        public static int QUERY_LAMP_FAILURE = 0x02;
        public static int QUERY_LAMP_POWER_ON = 0x03;
        public static int QUERY_LIMIT_ERROR = 0x04;
        public static int QUERY_RESET_STATE = 0x05;
        public static int QUERY_MISSING_SHORT_ADDRESS = 0x06;
        public static int QUERY_VERSION_NUMBER = 0x07;
        public static int QUERY_CONTENT_DTR = 0x08;
        public static int QUERY_CONTENT_DRT1 = 0x09;
        public static int QUERY_CONTENT_DTR2 = 0x0A;
        public static int QUERY_DEVICE_TYPE = 0x0B;
        public static int QUERY_PHYSICAL_MINIMUM_LEVEL = 0x0C;
        public static int QUERY_POWER_FAILURE = 0x0D;
        public static int QUERY_MAX_LEVEL = 0x0E;
        public static int QUERY_MIN_LEVEL = 0x0F;
        public static int QUERY_FADE_TIME_OR_FADE_RATE = 0x10;
        public static int QUERY_GROUPS0_7 = 0x11;
        public static int QUERY_GROUPS8_15 = 0x12;
        public static int QUERY_MEMORY_LOCATION = 0x013;
        public static int QUERY_LAST_LOCATION = 0x14;
        public static int QUERY_EXTENDED_VERSION_NUMBER = 0x15;
        public static int QUERY_CCT_COOLEST = 0x16;
        public static int QUERY_PHYSICAL_COOLEST = 0x17;
        public static int QUERY_CCT_WARMEST = 0x18;
        public static int QUERY_PHYSICAL_WARMEST = 0x19;

        public static int QUERY_ACTUAL_LEVEL = 0x30;
        public static int QUERY_POWER_ON_LEVEL = 0x34;
        public static int QUERY_SYSTEM_FAILURE_LEVEL = 0x38;
        public static int QUERY_SCENE_VALUE = 0x3C;

        public static MeshCommand queryDevice(int address, int daliAddr, int query) {
            return queryDevice(address, daliAddr, query, new byte[0]);
        }

        /**
         * @param address
         * @param daliAddr
         * @param query    MeshCommand.Uart.QUERY_XXXX
         * @param values
         * @return
         */
        public static MeshCommand queryDevice(int address, int daliAddr, int query, byte[] values) {
            byte[] data = new byte[]{
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
            };
            data[0] = (byte) 0xA5;
            data[1] = (byte) COMMAND_TYPE_QUERY;
            data[2] = (byte) daliAddr;
            data[3] = (byte) query;
            if (values.length > 0 && values.length <= 6) {
                for (int i = 0; i < values.length; i++) {
                    data[i + 4] = values[i];
                }
            }
            return sendUartData(address, data);
        }

        public static int DISCOVER_ALL_CONTROL_GEAR_SHALL_REACT = 0x00;
        public static int DISCOVER_WITHOUT_SHORT_ADDRESS_SHALL_REACT = 0x01;
        public static int DISCOVER_ADDRESS_SHALL_REACT = 0x02;
        public static int DISCOVER_CHECK_BUS_DEVICE = 0x03;

        public static MeshCommand discoverDevice(int address, int discover) {
            byte[] values = new byte[0];
            return discoverDevice(address, discover, values);
        }


        /**
         * @param address
         * @param discover MeshCommand.Uart.DISCOVER_XXXX
         * @param values
         * @return
         */
        public static MeshCommand discoverDevice(int address, int discover, byte[] values) {
            byte[] data = new byte[]{
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
            };
            data[0] = (byte) 0xA5;
            data[1] = (byte) COMMAND_TYPE_DISCOVER;
            data[2] = (byte) discover;
            if (values.length > 0 && values.length <= 7) {
                for (int i = 0; i < values.length; i++) {
                    data[i + 3] = values[i];
                }
            }
            return sendUartData(address, data);
        }

        public static MeshCommand terminateDiscovering(int address) {
            byte[] data = new byte[]{
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
            };
            data[0] = (byte) 0xA5;
            data[1] = (byte) COMMAND_TYPE_TERMINATE;
            return sendUartData(address, data);
        }

        /// - Parameters:
        ///     - address: The short address of the ble.
        ///     - switchId: The smart switch ID, such as `0x7000000D`.
        public static MeshCommand setSmartSwitchId(int address, String buttonPosition, int switchId) {
            MeshCommand cmd = new MeshCommand();
            cmd.tag = Const.TAG_APP_TO_NODE;
            cmd.dst = address;
            cmd.userData[0] = (byte) 0x12;
            cmd.userData[1] = (byte) 0x03;
            cmd.userData[2] = (byte) 0x01;
            cmd.userData[3] = SmartSwitchActions.ButtonPosition.getGroupIndex(buttonPosition);
            cmd.userData[4] = (byte) ((switchId >> 24) & 0xFF);
            cmd.userData[5] = (byte) ((switchId >> 16) & 0xFF);
            cmd.userData[6] = (byte) ((switchId >> 8) & 0xFF);
            cmd.userData[7] = (byte) (switchId & 0xFF);
            return cmd;
        }

        public static MeshCommand deleteSmartSwitch(int address, String buttonPosition, int switchId) {
            MeshCommand cmd = new MeshCommand();
            cmd.tag = Const.TAG_APP_TO_NODE;
            cmd.dst = address;
            cmd.userData[0] = (byte) 0x12;
            cmd.userData[1] = (byte) 0x03;
            cmd.userData[2] = (byte) 0x02;
            cmd.userData[3] = SmartSwitchActions.ButtonPosition.getGroupIndex(buttonPosition);
            cmd.userData[4] = (byte) ((switchId >> 24) & 0xFF);
            cmd.userData[5] = (byte) ((switchId >> 16) & 0xFF);
            cmd.userData[6] = (byte) ((switchId >> 8) & 0xFF);
            cmd.userData[7] = (byte) (switchId & 0xFF);
            return cmd;
        }

        /// buttonEvent use MeshCommand.SmartSwitchActions.ButtonEvent.
        private static List<MeshCommand> setSmartSwitchAction(int address, byte keyIndex, byte buttonEvent, byte[] data) {
            List<MeshCommand> commands = new ArrayList<>();

            MeshCommand cmd1 = new MeshCommand();
            cmd1.tag = Const.TAG_APP_TO_NODE;
            cmd1.dst = address;
            cmd1.userData[0] = (byte) 0x14;
            cmd1.userData[1] = (byte) 0x71;
            cmd1.userData[2] = keyIndex;
            cmd1.userData[3] = buttonEvent;
            cmd1.userData[4] = data[0];
            cmd1.userData[5] = data[1];
            cmd1.userData[6] = data[2];
            cmd1.userData[7] = data[3];
            cmd1.userData[8] = data[4];
            commands.add(cmd1);

            MeshCommand cmd2 = new MeshCommand();
            cmd2.tag = Const.TAG_APP_TO_NODE;
            cmd2.dst = address;
            cmd2.userData[0] = (byte) 0x14;
            cmd2.userData[1] = (byte) 0x72;
            cmd2.userData[2] = keyIndex;
            cmd2.userData[3] = buttonEvent;
            cmd2.userData[4] = data[5];
            cmd2.userData[5] = data[6];
            cmd2.userData[6] = data[7];
            cmd2.userData[7] = data[8];
            cmd2.userData[8] = data[9];
            commands.add(cmd2);

            return commands;
        }

        public static MeshCommand getSmartSwitchId(int address, int index) {
            MeshCommand cmd = new MeshCommand();
            cmd.tag = Const.TAG_APP_TO_NODE;
            cmd.dst = address;
            cmd.userData[0] = (byte) 0x72;
            cmd.userData[1] = (byte) index;
            return cmd;
        }

        public static class DaliAction {

            public static class Target {
                public enum TargetType {
                    device, group, broadcast
                }

                private TargetType targetType;
                private int deviceAddress = 0;
                private int groupId = 0;

                public TargetType getTargetType() {
                    return targetType;
                }

                public void setTargetType(TargetType targetType) {
                    this.targetType = targetType;
                }

                public int getDeviceAddress() {
                    return deviceAddress;
                }

                public void setDeviceAddress(int deviceAddress) {
                    this.deviceAddress = deviceAddress;
                }

                public int getGroupId() {
                    return groupId;
                }

                public void setGroupId(int groupId) {
                    this.groupId = groupId;
                }

                public Target(TargetType targetType) {
                    this.targetType = targetType;
                }

                public byte getDaliAddress() {
                    switch (targetType) {
                        case device:
                            return (byte) deviceAddress;
                        case group:
                            return (byte) (0x80 | groupId);
                        default:
                            return (byte) 0xFF;
                    }
                }
            }

            public static class Action {
                public static class ActionType {
                    public static final byte none = (byte) 0xFF;
                    public static final byte off = (byte) 0x01;
                    public static final byte level = (byte) 0x00;
                    public static final byte goToLastLevel = (byte) 0x0B;
                    public static final byte levelStepUp = (byte) 0x04;
                    public static final byte levelStepDown = (byte) 0x05;
                    public static final byte levelStepDownAndOff = (byte) 0x06;
                    public static final byte goToScene = (byte) 0x0A;
                }

                private byte actionType = ActionType.off;
                /// Range 0x00-0xFE, default is 254.
                private int level = 254;
                /// Range 0x00-0x0F, default is 0.
                private int sceneId = 0;

                public byte getActionType() {
                    return actionType;
                }

                public void setActionType(byte actionType) {
                    this.actionType = actionType;
                }

                public int getLevel() {
                    return level;
                }

                public void setLevel(int level) {
                    this.level = level;
                }

                public int getSceneId() {
                    return sceneId;
                }

                public void setSceneId(int sceneId) {
                    this.sceneId = sceneId;
                }

                public byte getActionValue() {
                    switch (actionType) {
                        case ActionType.level:
                            return (byte) level;
                        case ActionType.goToScene:
                            return (byte) sceneId;
                        default:
                            return 0;
                    }
                }

                /// actionType use UartDali.DaliAction.Action.ActionType
                public Action(byte actionType) {
                    this.actionType = actionType;
                }
            }

            private Target target;
            private Action action;

            public DaliAction(Target target, Action action) {
                this.target = target;
                this.action = action;
            }

            public Target getTarget() {
                return target;
            }

            public void setTarget(Target target) {
                this.target = target;
            }

            public Action getAction() {
                return action;
            }

            public void setAction(Action action) {
                this.action = action;
            }

        }

        public static List<MeshCommand> setSmartSwitch(int address, int switchId, String buttonPosition, byte buttonEvent, DaliAction daliAction) {
            List<MeshCommand> commands = new ArrayList<>();

            MeshCommand setCommand = MeshCommand.UartDali.setSmartSwitchId(address, buttonPosition, switchId);
            commands.add(setCommand);

            byte daliAddress = daliAction.getTarget().getDaliAddress();
            byte daliActionType = daliAction.getAction().getActionType();
            byte daliActionValue = daliAction.getAction().getActionValue();
            byte[] data = {(byte) 0xA5, (byte) 0x02, daliAddress, daliActionType, daliActionValue,
                    (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF};

            List<MeshCommand> actionCommands = MeshCommand.UartDali.setSmartSwitchAction(address, SmartSwitchActions.ButtonPosition.getKeyIndex(buttonPosition), buttonEvent, data);
            commands.addAll(actionCommands);

            return commands;
        }
    }

    public static class BleGateway {

        public static String updateLightRunningMode(LightRunningMode mode) {
            byte addrLow = (byte) (mode.address & 0xFF);
            byte addrHigh = (byte) ((mode.address >> 8) & 0xFF);
            byte userData3 = 0x00;
            byte userData4 = 0x00;
            switch (mode.state) {
                case LightRunningMode.State.DEFAULT_MODE: {
                    userData3 = (byte) mode.defaultMode;
                    break;
                }
                case LightRunningMode.State.CUSTOM_MODE: {
                    userData3 = (byte) mode.customModeId;
                    userData4 = (byte) mode.customMode;
                    break;
                }
            }
            byte[] data = new byte[]{
                    (byte) 0xBD, (byte) 0xBE, (byte) 0x03, (byte) 0x04,
                    addrHigh, addrLow,
                    (byte) 0x06, // payload data length
                    (byte) 0xEA, // custom protocol
                    (byte) Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE, // userData[0], 0x01
                    (byte) Const.LIGHT_CONTROL_MODE_SET_LIGHT_RUNNING_MODE, // userData[1], 0x05
                    (byte) mode.state, // userData[2]
                    userData3,
                    userData4
            };
            return HexUtil.getStringByBytes(data);
        }

        /**
         * @param address
         * @param speed   range [0x00, 0x0F], 0x00 -> fastest, 0x0F -> slowest.
         */
        public static String updateLightRunningSpeed(int address, int speed) {
            byte addrLow = (byte) (address & 0xFF);
            byte addrHigh = (byte) ((address >> 8) & 0xFF);
            byte[] data = new byte[]{
                    (byte) 0xBD, (byte) 0xBE, (byte) 0x03, (byte) 0x04,
                    addrHigh, addrLow,
                    (byte) 0x04, // payload data length
                    (byte) 0xEA, // custom protocol
                    (byte) Const.SR_IDENTIFIER_LIGHT_CONTROL_MODE, // userData[0]
                    (byte) Const.LIGHT_CONTROL_MODE_SET_LIGHT_RUNNING_SPEED, // userData[1]
                    (byte) speed, // userData[2]
            };
            return HexUtil.getStringByBytes(data);
        }

    } // BleGateway

    public static class UniversalKeyType {
        public static final byte shortPress = 1;
        public static final byte longPress = 2;
        public static final byte rotationOrLongPressEnding = 3;
    }

    /// remoteIndex 1, 2
    public static MeshCommand getUniversalRemoteId(int address, int remoteIndex) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x06; // universal remote
        cmd.userData[2] = (byte) 0x00; // get (read)
        cmd.userData[3] = (byte) remoteIndex;
        return cmd;
    }

    /// remoteId must be a hex string, ex. the mac of the device is `010203040506` then the remote ID must be `03040506`.
    public static MeshCommand setUniversalRemoteId(int address, int remoteIndex, long remoteId) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x06; // universal remote
        cmd.userData[2] = (byte) 0x01; // bind
        cmd.userData[3] = (byte) remoteIndex;
        cmd.userData[4] = (byte) ((remoteId >> 24) & 0xFF);
        cmd.userData[5] = (byte) ((remoteId >> 16) & 0xFF);
        cmd.userData[6] = (byte) ((remoteId >> 8) & 0xFF);
        cmd.userData[7] = (byte) ((remoteId) & 0xFF);
        return cmd;
    }

    /// remoteIndex 1, 2
    public static MeshCommand unbindUniversalRemoteId(int address, int remoteIndex) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x06; // universal remote
        cmd.userData[2] = (byte) 0x02; // unbind
        cmd.userData[3] = (byte) remoteIndex;
        return cmd;
    }

    public static MeshCommand setUniversalRemoteAction(int address, int remoteIndex, UniversalRemoteAction action) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x16;
        cmd.userData[1] = (byte) 0x01; // set
        cmd.userData[2] = (byte) remoteIndex;
        cmd.userData[3] = action.keyIndex;
        cmd.userData[4] = action.keyType;
        cmd.userData[5] = UniversalActionType.getActionNo(action.actionType, action.isRotation());
        cmd.userData[6] = action.args[0];
        cmd.userData[7] = action.args[1];
        cmd.userData[8] = action.args[2];
        return cmd;
    }

    /// Send this command will response short press, long press and rotation actions together.
    public static MeshCommand getUniversalRemoteAction(int address, int remoteIndex, byte keyIndex) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x16;
        cmd.userData[1] = (byte) 0x00; // get
        cmd.userData[2] = (byte) remoteIndex;
        cmd.userData[3] = keyIndex;
        return cmd;
    }

    /// `MeshManager.shared.sendCommands(commands, intervalSeconds: 0.5)`
    /// You ave to send there commands with a 500ms interval.
    public static List<MeshCommand> saveUniversalRemoteActions(int address, int remoteIndex, long remoteId, List<UniversalRemoteAction> actions) {
        List<MeshCommand> commands = new ArrayList<>();
        // bind remote index
        MeshCommand setRemoteIdCmd = setUniversalRemoteId(address, remoteIndex, remoteId);
        commands.add(setRemoteIdCmd);
        // set actions
        for (UniversalRemoteAction action : actions) {
            MeshCommand actionCmd = setUniversalRemoteAction(address, remoteIndex, action);
            commands.add(actionCmd);
            // If this action needs to stop, add stop action for it automatically.
            UniversalRemoteAction stopAction = action.getStopAction();
            if (stopAction != null) {
                MeshCommand stopCmd = setUniversalRemoteAction(address, remoteIndex, stopAction);
                commands.add(stopCmd);
            }
        }
        return commands;
    }

    public static MeshCommand deleteUniversalRemoteId(int address, int remoteIndex) {
        return unbindUniversalRemoteId(address, remoteIndex);
    }

    public static class UniversalRemoteAction {
        private byte keyIndex;
        private byte keyType; // UniversalKeyType.shortPress, ...
        private byte actionType; // UniversalActionType
        private byte[] args = new byte[3];

        public boolean isRotation() {
            return keyIndex == 17;
        }

        public static UniversalRemoteAction makeRotationAction(byte actionType, int arg0, int arg1, int arg2) {
            UniversalRemoteAction action = new UniversalRemoteAction();
            action.keyIndex = (byte) 17;
            action.keyType = UniversalKeyType.rotationOrLongPressEnding;
            action.actionType = actionType;
            byte[] args = new byte[]{(byte) arg0, (byte) arg1, (byte) arg2};
            action.setArgs(args);
            return action;
        }

        public static UniversalRemoteAction makeKeyAction(byte keyIndex, byte keyType, byte actionType, int arg0, int arg1, int arg2) {
            UniversalRemoteAction action = new UniversalRemoteAction();
            action.keyIndex = keyIndex;
            action.keyType = keyType;
            action.actionType = actionType;
            byte[] args = new byte[]{(byte) arg0, (byte) arg1, (byte) arg2};
            action.setArgs(args);
            return action;
        }

        public static UniversalRemoteAction makeSetCtOrWhiteAction(byte keyIndex, int ctOrWhite, boolean isRgbEnabled) {
            return makeKeyAction(keyIndex, UniversalKeyType.shortPress, UniversalActionType.setCt, ctOrWhite, isRgbEnabled ? 0 : 1, 0);
        }

        public static UniversalRemoteAction makeSetRedAction(byte keyIndex, int red, boolean isCtOrWhiteEnabled, boolean isRgbEnabled) {
            return makeKeyAction(keyIndex, UniversalKeyType.shortPress, UniversalActionType.setRed, red, isCtOrWhiteEnabled ? 0 : 1, isRgbEnabled ? 0 : 1);
        }

        public static UniversalRemoteAction makeSetGreenAction(byte keyIndex, int green, boolean isCtOrWhiteEnabled, boolean isRgbEnabled) {
            return makeKeyAction(keyIndex, UniversalKeyType.shortPress, UniversalActionType.setGreen, green, isCtOrWhiteEnabled ? 0 : 1, isRgbEnabled ? 0 : 1);
        }

        public static UniversalRemoteAction makeSetBlueAction(byte keyIndex, int blue, boolean isCtOrWhiteEnabled, boolean isRgbEnabled) {
            return makeKeyAction(keyIndex, UniversalKeyType.shortPress, UniversalActionType.setBlue, blue, isCtOrWhiteEnabled ? 0 : 1, isRgbEnabled ? 0 : 1);
        }

        public static List<UniversalRemoteAction> makeK5Actions() {
            List<UniversalRemoteAction> actions = new ArrayList<>();
            actions.add(makeRotationAction(UniversalActionType.none, 0, 0, 0));
            // keys 1, 2, 3, 4, 5
            actions.add(makeKeyAction((byte) 1, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 1, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 2, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 2, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 3, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 3, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 4, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 4, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 5, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 5, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            return actions;
        }

        public static List<UniversalRemoteAction> makeK12Actions() {
            List<UniversalRemoteAction> actions = new ArrayList<>();
            actions.add(makeRotationAction(UniversalActionType.none, 0, 0, 0));
            // keys 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 15, 16
            actions.add(makeKeyAction((byte) 1, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 1, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 2, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 2, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 3, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 3, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 4, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 4, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 5, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 5, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 6, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 6, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 7, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 7, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 8, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 8, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            /// Key index 9 is the rotation click and press key index.
            actions.add(makeKeyAction((byte) 9, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 9, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 11, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 11, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 12, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 12, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 15, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 15, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 16, UniversalKeyType.shortPress, UniversalActionType.none, 0, 0, 0));
            actions.add(makeKeyAction((byte) 16, UniversalKeyType.longPress, UniversalActionType.none, 0, 0, 0));
            return actions;
        }

        public static List<UniversalRemoteAction> copyActionsWithRemoteType(MeshDeviceType.UniversalRemoteType remoteType, List<UniversalRemoteAction> sourceActions) {
            List<UniversalRemoteAction> actions = new ArrayList<>();
            switch (remoteType) {
                case none:
                    break;
                case k5WithKnob:
                    actions = UniversalRemoteAction.makeK5Actions();
                case k12WithKnob:
                    actions = UniversalRemoteAction.makeK12Actions();
            }
            for (UniversalRemoteAction source : sourceActions) {
                if (source.isRotation()) {
                    // rotation shouldn't check the keyIndex or keyType
                    for (int i = 0; i < actions.size(); i++) {
                        if (actions.get(i).isRotation()) {
                            actions.set(i, source);
                            break;
                        }
                    }
                } else {
                    // if the keyIndex and keyType are the same, replace the action
                    for (int i = 0; i < actions.size(); i++) {
                        UniversalRemoteAction action = actions.get(i);
                        if (action.keyIndex == source.keyIndex && action.keyType == source.keyType) {
                            actions.set(i, source);
                            break;
                        }
                    }
                }
            }
            return actions;
        }

        public UniversalRemoteAction getStopAction() {
            Byte stopType = UniversalActionType.getStopActionType(actionType);
            if (stopType == null) {
                return null;
            }
            UniversalRemoteAction action = new UniversalRemoteAction();
            action.keyIndex = keyIndex;
            action.keyType = UniversalKeyType.rotationOrLongPressEnding;
            action.actionType = stopType;
            action.args = new byte[3];
            return action;
        }

        public List<Byte> getActionTypes() {
            if (isRotation()) {
                return UniversalActionType.rotationActionTypes;
            }
            switch (keyType) {
                case UniversalKeyType.shortPress:
                    return UniversalActionType.shortPressActionTypes;
                case UniversalKeyType.longPress:
                    return UniversalActionType.longPressActionTypes;
                default:
                    return new ArrayList<>();
            }
        }

        public byte getKeyIndex() {
            return keyIndex;
        }

        public void setKeyIndex(byte keyIndex) {
            this.keyIndex = keyIndex;
        }

        public byte getKeyType() {
            return keyType;
        }

        public void setKeyType(byte keyType) {
            this.keyType = keyType;
        }

        public byte getActionType() {
            return actionType;
        }

        public void setActionType(byte actionType) {
            this.actionType = actionType;
        }

        public byte[] getArgs() {
            return args;
        }

        public void setArgs(byte[] args) {
            this.args = args;
        }
    }

    public static final class UniversalActionType {
        public static final byte none = (byte) 0x00;

        public static final byte lightOn = (byte) 0x01;
        public static final byte lightOff = (byte) 0x02;
        public static final byte lightOnOff = (byte) 0x03;

        public static final byte whiteOnRgbEnableForRgbw = (byte) 0x04;
        public static final byte whiteOffRgbEnableForRgbw = (byte) 0x05;
        public static final byte whiteOnOffRgbEnableForRgbw = (byte) 0x06;

        public static final byte stepLevelUp = (byte) 0x07;
        public static final byte stepLevelDown = (byte) 0x08;
        public static final byte stepLevelUpDown = (byte) 0x09;

        public static final byte stepCtOrWhiteUpEnableRgb = (byte) 0x0A;
        public static final byte stepCtOrWhiteDownEnableRgb = (byte) 0x0B;
        public static final byte stepCtOrWhiteUpDownEnableRgb = (byte) 0x0C;
        public static final byte stepCtOrWhiteUpDisableRgb = (byte) 0x0D;
        public static final byte stepCtOrWhiteDownDisableRgb = (byte) 0x0E;
        public static final byte stepCtOrWhiteUpDownDisableRgb = (byte) 0x0F;

        public static final byte moveLevelStop = (byte) 0x10;
        public static final byte moveLevelUp = (byte) 0x11;
        public static final byte moveLevelDown = (byte) 0x12;
        public static final byte moveLevelUpDown = (byte) 0x13;
        public static final byte moveCtOrWhiteStopEnableRgb = (byte) 0x14;
        public static final byte moveCtOrWhiteUpEnableRgb = (byte) 0x15;
        public static final byte moveCtOrWhiteDownEnableRgb = (byte) 0x16;
        public static final byte moveCtOrWhiteUpDownEnableRgb = (byte) 0x17;
        public static final byte moveCtOrWhiteStopDisableRgb = (byte) 0x18;
        public static final byte moveCtOrWhiteUpDisableRgb = (byte) 0x19;
        public static final byte moveCtOrWhiteDownDisableRgb = (byte) 0x1A;
        public static final byte moveCtOrWhiteUpDownDisableRgb = (byte) 0x1B;
        public static final byte moveRgbMixFadeStopEnableCtOrWhite = (byte) 0x1C;
        public static final byte moveRgbMixFadeUpEnableCtOrWhite = (byte) 0x1D;
        public static final byte moveRgbMixFadeDownEnableCtOrWhite = (byte) 0x1E;
        public static final byte moveRgbMixFadeUpDownEnableCtOrWhite = (byte) 0x1F;
        public static final byte moveRgbMixFadeStopDisableCtOrWhite = (byte) 0x20;
        public static final byte moveRgbMixFadeUpDisableCtOrWhite = (byte) 0x21;
        public static final byte moveRgbMixFadeDownDisableCtOrWhite = (byte) 0x22;
        public static final byte moveRgbMixFadeUpDownDisableCtOrWhite = (byte) 0x23;
        public static final byte stopRunningSpeedChanging = (byte) 0x24;
        public static final byte runningSpeedUp = (byte) 0x25;
        public static final byte runningSpeedDown = (byte) 0x26;
        public static final byte runningSpeedUpDown = (byte) 0x27;
        public static final byte saveCustomScene1 = (byte) 0x28;
        public static final byte recallCustomScene1 = (byte) 0x29;
        public static final byte saveCustomScene2 = (byte) 0x2A;
        public static final byte recallCustomScene2 = (byte) 0x2B;
        public static final byte saveCustomScene3 = (byte) 0x2C;
        public static final byte recallCustomScene3 = (byte) 0x2D;
        public static final byte saveCustomScene4 = (byte) 0x2E;
        public static final byte recallCustomScene4 = (byte) 0x2F;
        public static final byte ctDefault3ModesLoopEnableRgb = (byte) 0x30;
        public static final byte ctDefault3ModesLoopDisableRgb = (byte) 0x31;
        public static final byte rgbDefault9ModesLoopEnableCtOrWhite = (byte) 0x32;
        public static final byte rgbDefault9ModesLoopDisableCtOrWhite = (byte) 0x33;
        public static final byte startBuiltin20RunningMode = (byte) 0x34;
        public static final byte setLevel = (byte) 0x35;
        /**
         * arg0 ctOrWhtie, arg1 (0 enable rgb, 1 disable rgb)
         */
        public static final byte setCt = (byte) 0x36;
        /**
         * rg0 red, arg1 (enable ct/w, 1 disable ct/w), arg2 (0 enable rgb, 1 disable rgb)
         */
        public static final byte setRed = (byte) 0x37;
        /**
         * as red
         */
        public static final byte setGreen = (byte) 0x38;
        /**
         * as red
         */
        public static final byte setBlue = (byte) 0x39;
        public static final byte setRgbEnableCtOrWhite = (byte) 0x3A;
        public static final byte setRgbDisableCtOrWhite = (byte) 0x3B;
        public static final byte savePrivateScene = (byte) 0x3C;
        public static final byte recallPrivateScene = (byte) 0x3D;
        public static final byte recallStandardScene = (byte) 0x3E;

        public static byte getActionNo(byte actionType, boolean isRotation) {
            if (isRotation) {
                switch (actionType) {
                    case setLevel:
                        return 0x01;
                    case setCt:
                        return 0x02;
                    case setRed:
                        return 0x03;
                    case setGreen:
                        return 0x04;
                    case setBlue:
                        return 0x05;
                    default:
                        break;
                }
            }
            return actionType;
        }

        public static byte makeRotationActionType(byte rawValue) {
            switch (rawValue) {
                case 0x01:
                    return setLevel;
                case 0x02:
                    return setCt;
                case 0x03:
                    return setRed;
                case 0x04:
                    return setGreen;
                case 0x05:
                    return setBlue;
                default:
                    return none;
            }
        }

        public static int getArgsCount(byte actionType) {
            switch (actionType) {
                case setLevel:
                case recallStandardScene:
                case savePrivateScene:
                case recallPrivateScene:
                    return 1;
                case setCt:
                    return 2;
                case setRed:
                case setGreen:
                case setBlue:
                case setRgbEnableCtOrWhite:
                case setRgbDisableCtOrWhite:
                    return 3;
                default:
                    return 0;
            }
        }

        public static final List<Byte> rotationActionTypes = new ArrayList<>(
                Arrays.asList(none, setLevel, setCt, setRed, setGreen, setBlue));

        public static final List<Byte> shortPressActionTypes = new ArrayList<>(
                Arrays.asList(
                        none,
                        lightOn, lightOff, lightOnOff,
                        stepLevelUp, stepLevelDown, stepLevelUpDown,
                        stepCtOrWhiteUpEnableRgb, stepCtOrWhiteDownEnableRgb, stepCtOrWhiteUpDownEnableRgb,
                        stepCtOrWhiteUpDisableRgb, stepCtOrWhiteDownDisableRgb, stepCtOrWhiteUpDownDisableRgb,
                        recallCustomScene1, recallCustomScene2, recallCustomScene3, recallCustomScene4,
                        ctDefault3ModesLoopEnableRgb, ctDefault3ModesLoopDisableRgb,
                        rgbDefault9ModesLoopEnableCtOrWhite, rgbDefault9ModesLoopDisableCtOrWhite,
                        startBuiltin20RunningMode,
                        setLevel, setCt, setRed, setGreen, setBlue,
                        setRgbEnableCtOrWhite, setRgbDisableCtOrWhite,
                        recallStandardScene
                ));

        public static final List<Byte> longPressActionTypes = new ArrayList<>(
                Arrays.asList(
                        none,
                        moveLevelUp, moveLevelDown, moveLevelUpDown,
                        moveCtOrWhiteUpEnableRgb, moveCtOrWhiteDownEnableRgb, moveCtOrWhiteUpDownEnableRgb,
                        moveCtOrWhiteUpDisableRgb, moveCtOrWhiteDownDisableRgb, moveCtOrWhiteUpDownDisableRgb,
                        moveRgbMixFadeUpEnableCtOrWhite, moveRgbMixFadeDownEnableCtOrWhite, moveRgbMixFadeUpDownEnableCtOrWhite,
                        moveRgbMixFadeUpDisableCtOrWhite, moveRgbMixFadeDownDisableCtOrWhite, moveRgbMixFadeUpDownDisableCtOrWhite,
                        runningSpeedUp, runningSpeedDown, runningSpeedUpDown,
                        saveCustomScene1, saveCustomScene2, saveCustomScene3, saveCustomScene4
                ));

        public static Byte getStopActionType(byte actionType) {
            switch (actionType) {
                case moveLevelUp:
                case moveLevelDown:
                case moveLevelUpDown:
                    return moveLevelStop;
                case moveCtOrWhiteUpEnableRgb:
                case moveCtOrWhiteDownEnableRgb:
                case moveCtOrWhiteUpDownEnableRgb:
                    return moveCtOrWhiteStopEnableRgb;
                case moveCtOrWhiteUpDisableRgb:
                case moveCtOrWhiteDownDisableRgb:
                case moveCtOrWhiteUpDownDisableRgb:
                    return moveCtOrWhiteStopDisableRgb;
                case moveRgbMixFadeUpEnableCtOrWhite:
                case moveRgbMixFadeDownEnableCtOrWhite:
                case moveRgbMixFadeUpDownEnableCtOrWhite:
                    return moveRgbMixFadeStopEnableCtOrWhite;
                case moveRgbMixFadeUpDisableCtOrWhite:
                case moveRgbMixFadeDownDisableCtOrWhite:
                case moveRgbMixFadeUpDownDisableCtOrWhite:
                    return moveRgbMixFadeStopDisableCtOrWhite;
                case runningSpeedUp:
                case runningSpeedDown:
                case runningSpeedUpDown:
                    return stopRunningSpeedChanging;
                default:
                    return null;
            }
        }
    }

    public static class MultiSensorAction {
        private final byte command = 0x19;
        // range is [1, 4]
        private int sensorIndex = 1;
        // range is [1, 2]
        private int actionIndex = 1;
        private byte actionNo = ActionNo.undefined;
        private int arg1 = 0x00;
        private int arg2 = 0x00;
        private int arg3 = 0x00;

        public static class ActionNo {
            public static final byte undefined = 0;
            public static final byte turnOn = 1;
            public static final byte turnOff = 2;
            public static final byte setBrightness = 0x35;
            public static final byte setCct = 0x36;
            public static final byte setRgbEnableCctWhite = 0x3A;
            public static final byte setRgbDisableCctWhite = 0x3B;
            public static final byte recallScene = 0x3E;
            public static final byte setLightMode = 0x3F;
        }

        public static class LightMode {
            public static final int turnOn = 1;
            public static final int turnOff = 2;
            public static final int flash0_5Hz = 3;
            public static final int flash1Hz = 4;
            public static final int flash2Hz = 5;
            public static final int flash3Hz = 6;
        }

        public static class BrightnessMode {
            public static final int current = 1;
            public static final int percent100 = 2;
            public static final int percent50 = 3;
            public static final int percent25 = 4;
        }

        public static class ColorMode {
            public static final int current = 1;
            public static final int red = 2;
            public static final int green = 3;
            public static final int blue = 4;
            /// redGreen
            public static final int yellow = 5;
            /// greenBlue
            public static final int cyan = 6;
            /// redBlue
            public static final int magenta = 7;
            public static final int warmWhite = 8;
            public static final int coolWhite = 9;
            public static final int white = 10;
        }

        public enum SensorType {
            unknown, doorContact, waterLeak
        }

        public MultiSensorAction(int sensorIndex, int actionIndex, byte actionNo, int arg1, int arg2, int arg3) {
            this.sensorIndex = sensorIndex;
            this.actionIndex = actionIndex;
            this.actionNo = actionNo;
            this.arg1 = arg1;
            this.arg2 = arg2;
            this.arg3 = arg3;
        }

        public MultiSensorAction(int sensorIndex, int actionIndex, byte actionNo) {
            this.sensorIndex = sensorIndex;
            this.actionIndex = actionIndex;
            this.actionNo = actionNo;
        }

        public MultiSensorAction(int sensorIndex, int actionIndex, byte actionNo, int arg1) {
            this.sensorIndex = sensorIndex;
            this.actionIndex = actionIndex;
            this.actionNo = actionNo;
            this.arg1 = arg1;
        }

        public static MultiSensorAction makeUndefinedAction(int sensorIndex, int actionIndex) {
            return new MultiSensorAction(sensorIndex, actionIndex, ActionNo.undefined);
        }

        public static MultiSensorAction makeTurnOnAction(int sensorIndex, int actionIndex) {
            return new MultiSensorAction(sensorIndex, actionIndex, ActionNo.turnOn);
        }

        public static MultiSensorAction makeTurnOffAction(int sensorIndex, int actionIndex) {
            return new MultiSensorAction(sensorIndex, actionIndex, ActionNo.turnOff);
        }

        public static MultiSensorAction makeSetBrightnessAction(int sensorIndex, int actionIndex, int brightness) {
            return new MultiSensorAction(sensorIndex, actionIndex, ActionNo.setBrightness, brightness);
        }

        public static MultiSensorAction makeSetCctAction(int sensorIndex, int actionIndex, int cct, boolean isRgbEnabled) {
            return new MultiSensorAction(sensorIndex, actionIndex, ActionNo.setCct, cct, isRgbEnabled ? 0x00 : 0x01, 0);
        }

        public static MultiSensorAction makeSetRgbAction(int sensorIndex, int actionIndex, int red, int green, int blue, boolean isCctWhiteEnabled) {
            return new MultiSensorAction(sensorIndex, actionIndex, isCctWhiteEnabled ? ActionNo.setRgbEnableCctWhite : ActionNo.setRgbDisableCctWhite, red, green, blue);
        }

        public static MultiSensorAction makeRecallSceneAction(int sensorIndex, int actionIndex, int sceneId) {
            return new MultiSensorAction(sensorIndex, actionIndex, ActionNo.recallScene, sceneId);
        }

        public static MultiSensorAction makeSetLightModeAction(int sensorIndex, int actionIndex, int lightMode, int brightnessMode, int colorMode) {
            return new MultiSensorAction(sensorIndex, actionIndex, ActionNo.setLightMode, lightMode, brightnessMode, colorMode);
        }

        @Nullable
        public static MultiSensorAction makeActionWithUserData(byte[] userData) {
            if (userData.length < 9) {
                Log.w("makeActionWithUserData", "makeActionWithUserData failed, userData.length < 9");
                return null;
            }
            if (userData[0] != 0x19) {
                Log.w("makeActionWithUserData", "makeActionWithUserData failed, userData.first != 0x19");
                return null;
            }
            int sensorIndex = (int) userData[2] & 0xFF;
            int actionIndex = ((int) userData[4] & 0xFF) + 1;
            if (actionIndex > 2) {
                Log.w("makeActionWithUserData", "makeActionWithUserData failed, actionIndex is bigger than 2, " + actionIndex);
                return null;
            }
            byte actionNo = userData[5];
            return new MultiSensorAction(sensorIndex, actionIndex, actionNo, (int) userData[6] & 0xFF, (int) userData[7] & 0xFF, (int) userData[8] & 0xFF);
        }

        public int getSensorIndex() {
            return sensorIndex;
        }

        public void setSensorIndex(int sensorIndex) {
            this.sensorIndex = sensorIndex;
        }

        public int getActionIndex() {
            return actionIndex;
        }

        public void setActionIndex(int actionIndex) {
            this.actionIndex = actionIndex;
        }

        public byte getActionNo() {
            return actionNo;
        }

        public void setActionNo(byte actionNo) {
            this.actionNo = actionNo;
        }

        public int getArg1() {
            return arg1;
        }

        public void setArg1(int arg1) {
            this.arg1 = arg1;
        }

        public int getArg2() {
            return arg2;
        }

        public void setArg2(int arg2) {
            this.arg2 = arg2;
        }

        public int getArg3() {
            return arg3;
        }

        public void setArg3(int arg3) {
            this.arg3 = arg3;
        }
    }


    // sensorId just like `0x9000000C`
    public static List<MeshCommand> linkMultiSensor(int address, long sensorId, MultiSensorAction action1, MultiSensorAction action2, MultiSensorAction.SensorType sensorType) {
        MeshCommand clearCommand = null;
        if (sensorType == MultiSensorAction.SensorType.doorContact) {
            clearCommand = clearManualLinkedDoorSensorId(address);
        } else if (sensorType == MultiSensorAction.SensorType.waterLeak) {
            clearCommand = clearManualLinkedWaterLeakSensorId(address);
        }
        MeshCommand idCommand = MeshCommand.linkMultiSensorId(address, sensorId, action1.sensorIndex);
        MeshCommand action1Command = MeshCommand.linkMultiSensorAction(address, action1);
        MeshCommand action2Command = linkMultiSensorAction(address, action2);
        if (clearCommand != null) {
            return Arrays.asList(clearCommand, idCommand, action1Command, action2Command);
        }
        return Arrays.asList(idCommand, action1Command, action2Command);
    }

    public static MeshCommand linkMultiSensorId(int address, long sensorId, int sensorIndex) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x04; // bind sensor
        cmd.userData[2] = (byte) 0x01; // bind
        cmd.userData[3] = (byte) ((sensorIndex & 0x0F) | 0xF0);
        cmd.userData[4] = (byte) ((sensorId >> 24) & 0xFF);
        cmd.userData[5] = (byte) ((sensorId >> 16) & 0xFF);
        cmd.userData[6] = (byte) ((sensorId >> 8) & 0xFF);
        cmd.userData[7] = (byte) (sensorId & 0xFF);
        return cmd;
    }

    public static MeshCommand linkMultiSensorAction(int address, MultiSensorAction action) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x19;
        cmd.userData[1] = (byte) 0x01; // set action
        cmd.userData[2] = (byte) action.sensorIndex;
        cmd.userData[3] = (byte) 0x01; // reserved
        cmd.userData[4] = (byte) (action.actionIndex == 1 ? 0 : 1);
        cmd.userData[5] = action.actionNo;
        cmd.userData[6] = (byte) action.arg1;
        cmd.userData[7] = (byte) action.arg2;
        cmd.userData[8] = (byte) action.arg3;
        return cmd;
    }

    public static MeshCommand unlinkMultiSensorId(int address, int sensorIndex) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x04; // bind sensor
        cmd.userData[2] = (byte) 0x02; // unbind
        cmd.userData[3] = (byte) ((sensorIndex & 0x0F) | 0xF0);
        return cmd;
    }

    public static MeshCommand getLinkedMultiSensorId(int address, int sensorIndex) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x04; // bind sensor
        cmd.userData[2] = (byte) 0x00; // get
        cmd.userData[3] = (byte) ((sensorIndex & 0x0F) | 0xF0);
        return cmd;
    }

    public static MeshCommand getLinkedMultiSensorAction(int address, int sensorIndex, int actionIndex) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x19;
        cmd.userData[1] = (byte) 0x00; // get action
        cmd.userData[2] = (byte) sensorIndex;
        cmd.userData[3] = (byte) 0x01; // reserved
        cmd.userData[4] = (byte) (actionIndex == 1 ? 0 : 1);
        return cmd;
    }

    public static class SingleSensorAction {

        public byte sensorType = SensorType.doorContactSensor;
        // range is [1, 2]
        public int actionIndex = 1;
        public ActionNo actionNo = ActionNo.undefined;
        // [0, 100]
        public int brightness = 100;
        // [0, 255]
        public int red = 255;
        public int green = 255;
        public int blue = 255;
        // cct [0, 100], white [0, 255]
        public int cctOrWhite = 0;
        // scene ID [1, 254]
        public int sceneId = 1;
        public boolean isEnabled = true;
        // [0, 65535] seconds
        public int transition = 0;

        public byte getSensorTypeValue() {
            return actionIndex == 1 ? SensorType.getAction1Value(sensorType) : SensorType.getAction2Value(sensorType);
        }

        public byte getActionNoValue() {
            switch (actionNo) {
                case turnOn:
                case turnOff:
                    return (byte) 0x01;
                case setBrightness:
                    return (byte) 0x05;
                case setCctOrWhite:
                case setRgb:
                    return (byte) 0x06;
                case recallScene:
                    return (byte) 0x02;
                default:
                    return (byte) 0xF0;
            }
        }

        public SingleSensorAction(byte sensorType, int actionIndex, ActionNo actionNo) {
            this.sensorType = sensorType;
            this.actionIndex = actionIndex;
            this.actionNo = actionNo;
        }

        public static class SensorType {
            public static final byte doorContactSensor = (byte) 0x01; // closed, open
            public static final byte waterLeakSensor = (byte) 0x07; // normal, water leak detected
            public static final byte smokeSensor = (byte) 0x08; // normal, smoke detected
            public static final byte coSensor = (byte) 0x09; // normal, co detected
            public static final byte gasSensor = (byte) 0x0A; // normal, gas detected
            public static final byte airQualitySensor = (byte) 0x0B; // good, poor air quality
            public static final byte glassBreakSensor = (byte) 0x0C; // normal, glass break detected
            public static final byte vibrationSensor = (byte) 0x0D; // no vibration, vibration detected

            public static byte getAction1Value(byte sensorType) {
                return (byte) (sensorType | 0x50);
            }

            public static byte getAction2Value(byte sensorType) {
                return (byte) (sensorType | 0x40);
            }
        }

        public static enum ActionNo {
            undefined,
            turnOn,
            turnOff,
            setBrightness,
            setCctOrWhite,
            setRgb,
            recallScene
        }

        public byte getSensorType() {
            return sensorType;
        }

        public void setSensorType(byte sensorType) {
            this.sensorType = sensorType;
        }

        public int getActionIndex() {
            return actionIndex;
        }

        public void setActionIndex(int actionIndex) {
            this.actionIndex = actionIndex;
        }

        public ActionNo getActionNo() {
            return actionNo;
        }

        public void setActionNo(ActionNo actionNo) {
            this.actionNo = actionNo;
        }

        public int getBrightness() {
            return brightness;
        }

        public void setBrightness(int brightness) {
            this.brightness = brightness;
        }

        public int getRed() {
            return red;
        }

        public void setRed(int red) {
            this.red = red;
        }

        public int getGreen() {
            return green;
        }

        public void setGreen(int green) {
            this.green = green;
        }

        public int getBlue() {
            return blue;
        }

        public void setBlue(int blue) {
            this.blue = blue;
        }

        public int getCctOrWhite() {
            return cctOrWhite;
        }

        public void setCctOrWhite(int cctOrWhite) {
            this.cctOrWhite = cctOrWhite;
        }

        public int getSceneId() {
            return sceneId;
        }

        public void setSceneId(int sceneId) {
            this.sceneId = sceneId;
        }

        public boolean isEnabled() {
            return isEnabled;
        }

        public void setEnabled(boolean enabled) {
            isEnabled = enabled;
        }

        public int getTransition() {
            return transition;
        }

        public void setTransition(int transition) {
            this.transition = transition;
        }
    }

    // sensorId just like `0x9000000C`
    public static List<MeshCommand> linkSingleSensor(int address, long sensorId, SingleSensorAction action1, SingleSensorAction action2) {
        return Arrays.asList(
                linkSingleSensorId(address, sensorId, action1.sensorType),
                linkSingleSensorAction(address, action1),
                linkSingleSensorAction(address, action2)
        );
    }

    public static MeshCommand linkSingleSensorId(int address, long sensorId, byte sensorType) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x04; // bind sensor
        cmd.userData[2] = (byte) 0x01; // bind
        cmd.userData[3] = sensorType;
        cmd.userData[4] = (byte) ((sensorId >> 24) & 0xFF);
        cmd.userData[5] = (byte) ((sensorId >> 16) & 0xFF);
        cmd.userData[6] = (byte) ((sensorId >> 8) & 0xFF);
        cmd.userData[7] = (byte) (sensorId & 0xFF);
        return cmd;
    }

    public static MeshCommand linkSingleSensorAction(int address, SingleSensorAction action) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = action.getSensorTypeValue();
        cmd.userData[1] = action.getActionNoValue();
        switch (action.actionNo) {
            case undefined:
                break;
            case turnOn:
                cmd.userData[2] = (byte) 0x01;
                cmd.userData[6] = (byte) (action.transition & 0xFF);
                cmd.userData[7] = (byte) (action.transition >> 8);
                break;
            case turnOff:
                cmd.userData[2] = (byte) 0x00;
                cmd.userData[6] = (byte) (action.transition & 0xFF);
                cmd.userData[7] = (byte) (action.transition >> 8);
                break;
            case setBrightness:
                cmd.userData[2] = (byte) (action.brightness);
                cmd.userData[6] = (byte) (action.transition & 0xFF);
                cmd.userData[7] = (byte) (action.transition >> 8);
                break;
            case setCctOrWhite:
                cmd.userData[2] = (byte) 0x05; // ctw
                cmd.userData[3] = (byte) (action.cctOrWhite);
                break;
            case setRgb:
                cmd.userData[2] = (byte) 0x04; // rgb
                cmd.userData[3] = (byte) (action.red);
                cmd.userData[4] = (byte) (action.green);
                cmd.userData[5] = (byte) (action.blue);
                break;
            case recallScene:
                cmd.userData[2] = (byte) (action.sceneId);
                break;
        }
        return cmd;
    }

    public static MeshCommand unlinkSingleSensorId(int address, byte sensorType) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x04; // bind sensor
        cmd.userData[2] = (byte) 0x02; // unbind
        cmd.userData[3] = sensorType;
        return cmd;
    }

    public static MeshCommand getLinkedSingleSensorId(int address, byte sensorType) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x04; // bind sensor
        cmd.userData[2] = (byte) 0x00; // get
        cmd.userData[3] = sensorType;
        return cmd;
    }

    public static MeshCommand getLinkedSingleSensorAction(int address, byte sensorType, int actionIndex) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = actionIndex == 1 ? SingleSensorAction.SensorType.getAction1Value(sensorType) : SingleSensorAction.SensorType.getAction2Value(sensorType);
        cmd.userData[1] = (byte) 0x00; // get action
        return cmd;
    }

    public static Map<Integer, String> curtainState = new HashMap<>();

    public static class CurtainCalibrationReport {
        public boolean isSuccess;
        /// ms
        public int motorStartTime = 0;
        /// ms
        public int totalTravelTime = 0;

        /// return true if state is valid.
        public boolean setSuccessFromState(int state) {
            switch (state) {
                case 0x66:
                    isSuccess = true;
                    return true;
                case 0xE2:
                case 0xE3:
                    isSuccess = false;
                    return true;
                default:
                    return false;
            }
        }
    }

    public static MeshCommand openCurtain(int address, MeshDeviceType.CurtainType curtainType) {
        String newState = "Opening".equals(curtainState.get(address)) ? "Stopped" : "Opening";
        curtainState.put(address, newState);
        return turnOnOff(address, true);
    }

    public static MeshCommand closeCurtain(int address, MeshDeviceType.CurtainType curtainType) {
        String newState = "Closing".equals(curtainState.get(address)) ? "Stopped" : "Closing";
        curtainState.put(address, newState);
        return turnOnOff(address, false);
    }

    /// Note this function may return null.
    @Nullable
    public static MeshCommand stopCurtainMoving(int address, MeshDeviceType.CurtainType curtainType) {
        String state = curtainState.get(address) != null ? curtainState.get(address) : "Stopped";
        if (state == null) {
            return null;
        }
        if (state.equals("Opening")) {
            return openCurtain(address, curtainType);
        } else if (state.equals("Closing")) {
            return closeCurtain(address, curtainType);
        } else {
            return null;
        }
    }

    public static MeshCommand calibrateCurtain(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x25;
        cmd.userData[1] = (byte) 0xAC;
        return cmd;
    }

    public static MeshCommand getManualLinkedDoorSensorId(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x00;
        cmd.userData[2] = (byte) 0x10;
        return cmd;
    }

    public static MeshCommand getManualLinkedWaterLeakSensorId(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x00;
        cmd.userData[2] = (byte) 0x16;
        return cmd;
    }

    public static MeshCommand clearManualLinkedDoorSensorId(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x10;
        cmd.userData[2] = (byte) 0x02;
        return cmd;
    }

    public static MeshCommand clearManualLinkedWaterLeakSensorId(int address) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_APP_TO_NODE;
        cmd.dst = address;
        cmd.userData[0] = (byte) 0x12;
        cmd.userData[1] = (byte) 0x16;
        cmd.userData[2] = (byte) 0x02;
        return cmd;
    }

    public static MeshCommand setWww(int address, int w1, int w2, int w3, boolean ctwDisabled) {
        MeshCommand cmd = new MeshCommand();
        cmd.tag = Const.TAG_SINGLE_CHANNEL;
        cmd.dst = address;
        cmd.param = Const.SINGLE_CHANNEL_WWW;
        cmd.userData[0] = (byte) w1;
        cmd.userData[1] = (byte) w2;
        cmd.userData[2] = (byte) w3;
        cmd.userData[3] = ctwDisabled ? (byte) 1 : (byte) 0;
        return cmd;
    }

    public static MeshCommand setDoubleChannelsOnOff(int address, boolean isChannel1On, boolean isChannel2On) {
        return setWww(address, isChannel1On ? 1 : 0, isChannel2On ? 1 : 0, 0, false);
    }
}
